Skip to content

Commit

Permalink
v2 iteration on testing codeblocks
Browse files Browse the repository at this point in the history
This commit is v2 approach to testing for accessible colors and codeblock rendering using an in-house approach to identifying ANSI escape sequences and analyzing them for color depth.

After talking with @williammartin, this is likely going to be refactored a third time leveraging a module to help with parsing escape sequences from text.
  • Loading branch information
andyfeller committed Feb 24, 2025
1 parent e61b6d6 commit c5336a5
Show file tree
Hide file tree
Showing 6 changed files with 142 additions and 52 deletions.
4 changes: 1 addition & 3 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@ go 1.21
require (
github.com/AlecAivazis/survey/v2 v2.3.7
github.com/MakeNowJust/heredoc v1.0.0
github.com/charmbracelet/glamour v0.8.0
github.com/charmbracelet/lipgloss v1.0.0
github.com/cli/browser v1.3.0
github.com/cli/glamour v0.0.0-20250220192152-8544502ccff9
github.com/cli/safeexec v1.0.0
github.com/cli/shurcooL-graphql v0.0.4
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510
Expand Down Expand Up @@ -49,5 +49,3 @@ require (
golang.org/x/net v0.33.0 // indirect
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect
)

replace github.com/charmbracelet/glamour => github.com/cli/glamour v0.0.0-20250219132354-dba8cf7ee336
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,8 @@ github.com/charmbracelet/x/exp/golden v0.0.0-20240806155701-69247e0abc2a h1:G99k
github.com/charmbracelet/x/exp/golden v0.0.0-20240806155701-69247e0abc2a/go.mod h1:wDlXFlCrmJ8J+swcL/MnGUuYnqgQdW9rhSD61oNMb6U=
github.com/cli/browser v1.3.0 h1:LejqCrpWr+1pRqmEPDGnTZOjsMe7sehifLynZJuqJpo=
github.com/cli/browser v1.3.0/go.mod h1:HH8s+fOAxjhQoBUAsKuPCbqUuxZDhQ2/aD+SzsEfBTk=
github.com/cli/glamour v0.0.0-20250219132354-dba8cf7ee336 h1:8C3iXVZ6ZP8MblfrT1KKPMgW0q1ZO9nXSPvpKy79VEk=
github.com/cli/glamour v0.0.0-20250219132354-dba8cf7ee336/go.mod h1:OUgq8/2KmRHaylj8o+SGSoOiY+D0bNJfwaqN15oK4A0=
github.com/cli/glamour v0.0.0-20250220192152-8544502ccff9 h1:fPJNUzG+Au+pIfYx2c5QNngZ3KLj7xAzjutL6efy1x8=
github.com/cli/glamour v0.0.0-20250220192152-8544502ccff9/go.mod h1:bB4uNJ5F0+nzpGqwlhKEy7tD0PPL0SxNWEwZzG77vMg=
github.com/cli/safeexec v1.0.0 h1:0VngyaIyqACHdcMNWfo6+KdUYnqEr2Sg+bSP1pdF+dI=
github.com/cli/safeexec v1.0.0/go.mod h1:Z/D4tTN8Vs5gXYHDCbaM1S/anmEDnJb1iW0+EJ5zx3Q=
github.com/cli/shurcooL-graphql v0.0.4 h1:6MogPnQJLjKkaXPyGqPRXOI2qCsQdqNfUY1QSJu2GuY=
Expand Down
4 changes: 2 additions & 2 deletions pkg/markdown/accessibility.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ package markdown
import (
"strconv"

"github.com/charmbracelet/glamour/ansi"
"github.com/charmbracelet/glamour/styles"
"github.com/cli/glamour/ansi"
"github.com/cli/glamour/styles"
)

type ANSIColorCode int
Expand Down
4 changes: 2 additions & 2 deletions pkg/markdown/accessibility_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ package markdown
import (
"testing"

"github.com/charmbracelet/glamour/ansi"
"github.com/charmbracelet/glamour/styles"
"github.com/cli/glamour/ansi"
"github.com/cli/glamour/styles"
"github.com/stretchr/testify/assert"
)

Expand Down
2 changes: 1 addition & 1 deletion pkg/markdown/markdown.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import (
"os"
"strings"

"github.com/charmbracelet/glamour"
"github.com/cli/glamour"
"github.com/cli/go-gh/v2/pkg/accessibility"
)

Expand Down
176 changes: 134 additions & 42 deletions pkg/markdown/markdown_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import (
"fmt"
"os"
"path/filepath"
"regexp"
"strconv"
"strings"
"testing"

Expand All @@ -20,8 +22,140 @@ const (
customH2_8bitColorSeq = "\x1b[38;5;61;"
magenta_4bitColorSeq = "\x1b[35;"
brightMagenta_4bitColorSeq = "\x1b[95;"

// 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
// sgrSequencePattern identifies ANSI escape sequences containing display attributes
// that affect the color, emphasis, and other aspects of displaying text.
sgrSequencePattern = `\x1b\[(.+?)m`

// sgrAttributePattern analyzes separate display attributes within an ANSI escape sequence

Check failure on line 31 in pkg/markdown/markdown_test.go

View workflow job for this annotation

GitHub Actions / lint

Sentence should start with a capital letter (godot)
// for detecting color depth (3-bit, 4-bit, 8-bit, 24-bit, etc) or other effects.
//
// This is a separate regex from sgrSequencePattern as `FindAllStringSubmatch()` does not
// handle repeating capture groups well.
sgrAttributePattern = `;?(?P<sequence>\d+)` // TODO: change the `sequence` note; if we aren't actually using the name group, remove it
)

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

sequencesRegex := regexp.MustCompile(sgrSequencePattern)
attributesRegex := regexp.MustCompile(sgrAttributePattern)
text := heredoc.Docf(`
%[1]s%[1]s%[1]sgo
package main
import (
"fmt"
)
func main() {
fmt.Println("Hello, world!")
}
%[1]s%[1]s%[1]s
`, "`")

tests := []struct {
name string
text string
theme string
accessible bool
}{
{
name: "when the light theme is selected, the codeblock renders using 8-bit colors",
text: text,
theme: "light",
},
{
name: "when the dark theme is selected, the codeblock renders using 8-bit colors",
text: text,
theme: "dark",
},
{
name: "when the accessible env var is set and the light theme is selected, the codeblock renders using 4-bit colors",
text: text,
theme: "light",
accessible: true,
},
{
name: "when the accessible env var is set and the dark theme is selected, the codeblock renders using 4-bit colors",
text: text,
theme: "dark",
accessible: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if tt.accessible {
t.Setenv(accessibility.ACCESSIBILITY_ENV, "true")
}

out, err := Render(tt.text, WithTheme(tt.theme))
require.NoError(t, err)
sequences := sequencesRegex.FindAllStringSubmatch(out, -1)
require.NotEmpty(t, sequences, "Failed to find expected SGR sequences in rendered output")

// 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
for _, sequence := range sequences {
attributes := attributesRegex.FindAllStringSubmatch(sequence[1], -1)
require.NotEmpty(t, attributes, "Failed to extract SGR attributes for testing")

// Analysis loop handles index incrementing due to unique display attribute situations like color depth
for i := 0; i < len(attributes); {
// TODO: Use constants for 38,48 and maybe remove the int conversion
attribute, err := strconv.Atoi(attributes[i][1])
require.NoError(t, err, "Failed to convert SGR attribute for testing")

switch attribute {
case 38, 48:
// Display attributes for setting 8-bit and 24-bit foreground and background colors
colorDepth, err := strconv.Atoi(attributes[i+1][1])
require.NoError(t, err, "Failed to convert SGR color depth attribute for testing")

switch colorDepth {
case 2:
// 24-bit color display attribute form
// - ESC[38;2;⟨r⟩;⟨g⟩;⟨b⟩m for foreground colors
// - ESC[48;2;⟨r⟩;⟨g⟩;⟨b⟩m for background colors
require.False(t, tt.accessible, "24-bit color is not accessible, customizable")

color24bitRed, err := strconv.Atoi(attributes[i+2][1])
require.NoError(t, err, "Failed to convert 24-bit red color value for testing")
require.True(t, color24bitRed >= 0 && color24bitRed <= 255, "24-bit red color value out of 0-255 range")

color24bitGreen, err := strconv.Atoi(attributes[i+3][1])
require.NoError(t, err, "Failed to convert 24-bit green color value for testing")
require.True(t, color24bitGreen >= 0 && color24bitGreen <= 255, "24-bit green color value out of 0-255 range")

color24bitBlue, err := strconv.Atoi(attributes[i+4][1])
require.NoError(t, err, "Failed to convert 24-bit blue color value for testing")
require.True(t, color24bitBlue >= 0 && color24bitBlue <= 255, "24-bit blue color value out of 0-255 range")

i += 5
case 5:
// 8-bit color display attributes form:
// - ESC[38;5;⟨n⟩m for foreground colors
// - ESC[48;5;⟨n⟩m for background colors
require.False(t, tt.accessible, "8-bit color is not accessible, customizable")

color8bit, err := strconv.Atoi(attributes[i+2][1])
require.NoError(t, err, "Failed to convert 8-bit color value for testing")
require.True(t, color8bit >= 0 && color8bit <= 255, "8-bit color value out of 0-255 range")

i += 3
default:
require.Fail(t, "Unexpected color depth in attribute")
}
default:
// Increment index as this attribute does not affect accessibility currently
i += 1
}
}
}
})
}
}

// Test_Render verifies that the proper ANSI color codes are applied to the rendered
// markdown by examining the ANSI escape sequences in the output for the correct color
// match. For more information on ANSI color codes, see
Expand Down Expand Up @@ -75,48 +209,6 @@ func Test_Render(t *testing.T) {
accessibleEnvVar: "true",
wantOut: fmt.Sprintf("%s1mh2", brightMagenta_4bitColorSeq),
},
{
name: "when the light theme is selected, the codeblock renders using 8-bit colors",
text: heredoc.Docf(`
%[1]s%[1]s%[1]sgo
fmt.Println("Hello, world!")
%[1]s%[1]s%[1]s
`, "`"),
theme: "light",
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",
},
{
name: "when the dark theme is selected, the codeblock renders using 8-bit colors",
text: heredoc.Docf(`
%[1]s%[1]s%[1]sgo
fmt.Println("Hello, world!")
%[1]s%[1]s%[1]s
`, "`"),
theme: "dark",
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",
},
{
name: "when the accessible env var is set and the light theme is selected, the codeblock renders using 4-bit colors",
text: heredoc.Docf(`
%[1]s%[1]s%[1]sgo
fmt.Println("Hello, world!")
%[1]s%[1]s%[1]s
`, "`"),
theme: "light",
accessibleEnvVar: "true",
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",
},
{
name: "when the accessible env var is set and the dark theme is selected, the codeblock renders using 4-bit colors",
text: heredoc.Docf(`
%[1]s%[1]s%[1]sgo
fmt.Println("Hello, world!")
%[1]s%[1]s%[1]s
`, "`"),
theme: "dark",
accessibleEnvVar: "true",
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",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
Expand Down

0 comments on commit c5336a5

Please sign in to comment.