Skip to content

Commit 9704682

Browse files
committed
MINOR: support GitLab wiki formats and CRLF in remote files
Update the remote file fetching logic to robustly handle payloads from GitLab wiki pages and other markdown-formatted dictionary sources. Changes include: - Normalizing Windows-style CRLF line endings to standard LF. - Adding a stripCodeFence helper to safely remove markdown code blocks, regardless of whether a language tag (like yaml) is provided. - Enhancing fallback parsing to correctly process markdown list items by stripping "- " prefixes from newline-separated entries. - Adding a comprehensive test suite to validate parsing behavior across JSON, YAML, plain text, and Markdown formats with mixed line endings.
1 parent 3dbda6d commit 9704682

2 files changed

Lines changed: 97 additions & 10 deletions

File tree

aspell/remote.go

Lines changed: 27 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -70,18 +70,22 @@ func fetchRemoteFile(aspell Aspell) ([]string, error) {
7070
if !ok {
7171
return nil, fmt.Errorf("aspell remote file: key %q not found or not a string/array in response", aspell.RemoteFile.AllowedItemsKey)
7272
}
73-
content = strings.TrimRight(content, "\n")
74-
if strings.HasPrefix(content, "```yaml\n") && strings.HasSuffix(content, "\n```") {
75-
content = strings.TrimPrefix(content, "```yaml\n")
76-
content = strings.TrimSuffix(content, "\n```")
77-
err = yaml.Unmarshal([]byte(content), &allowedWords)
78-
if err != nil {
79-
return nil, fmt.Errorf("aspell remote file: failed to parse YAML block: %w", err)
80-
}
81-
slog.Info("aspell remote file: loaded words", "format", "yaml block", "count", len(allowedWords), "sample", wordSample(allowedWords))
73+
content = strings.ReplaceAll(content, "\r\n", "\n")
74+
content = strings.TrimSpace(content)
75+
content = stripCodeFence(content)
76+
if err = yaml.Unmarshal([]byte(content), &allowedWords); err == nil && len(allowedWords) > 0 {
77+
slog.Info("aspell remote file: loaded words", "format", "yaml list", "count", len(allowedWords), "sample", wordSample(allowedWords))
8278
return allowedWords, nil
8379
}
84-
allowedWords = strings.Split(content, "\n")
80+
allowedWords = allowedWords[:0]
81+
for line := range strings.SplitSeq(content, "\n") {
82+
line = strings.TrimSpace(line)
83+
line = strings.TrimPrefix(line, "- ")
84+
if line == "" {
85+
continue
86+
}
87+
allowedWords = append(allowedWords, line)
88+
}
8589
slog.Info("aspell remote file: loaded words", "format", "newline-separated", "count", len(allowedWords), "sample", wordSample(allowedWords))
8690
} else {
8791
for _, item := range items {
@@ -93,6 +97,19 @@ func fetchRemoteFile(aspell Aspell) ([]string, error) {
9397
return allowedWords, nil
9498
}
9599

100+
// stripCodeFence removes a surrounding markdown code fence, with or
101+
// without a language tag, as produced by GitLab wiki pages.
102+
func stripCodeFence(content string) string {
103+
if !strings.HasPrefix(content, "```") || !strings.HasSuffix(content, "\n```") {
104+
return content
105+
}
106+
_, body, found := strings.Cut(content, "\n")
107+
if !found {
108+
return content
109+
}
110+
return strings.TrimSuffix(body, "\n```")
111+
}
112+
96113
func wordSample(words []string) []string {
97114
if len(words) <= 3 {
98115
return words

aspell/remote_test.go

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
package aspell
2+
3+
import (
4+
"net/http"
5+
"net/http/httptest"
6+
"slices"
7+
"testing"
8+
)
9+
10+
func serveContent(t *testing.T, body string) *httptest.Server {
11+
t.Helper()
12+
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
13+
w.Header().Set("Content-Type", "application/json")
14+
_, _ = w.Write([]byte(body))
15+
}))
16+
t.Cleanup(srv.Close)
17+
return srv
18+
}
19+
20+
func Test_fetchRemoteFile_formats(t *testing.T) {
21+
tests := []struct {
22+
name string
23+
body string
24+
want []string
25+
}{
26+
{
27+
name: "gitlab wiki CRLF plain fence with list items",
28+
body: `{"content": "` + "```" + `\r\n- ACL\r\n- eab\r\n- verifier\r\n` + "```" + `"}`,
29+
want: []string{"ACL", "eab", "verifier"},
30+
},
31+
{
32+
name: "yaml block LF",
33+
body: `{"content": "` + "```yaml" + `\n- word1\n- word2\n` + "```" + `"}`,
34+
want: []string{"word1", "word2"},
35+
},
36+
{
37+
name: "yaml block CRLF",
38+
body: `{"content": "` + "```yaml" + `\r\n- word1\r\n- word2\r\n` + "```" + `"}`,
39+
want: []string{"word1", "word2"},
40+
},
41+
{
42+
name: "plain newline-separated",
43+
body: `{"content": "word1\nword2"}`,
44+
want: []string{"word1", "word2"},
45+
},
46+
{
47+
name: "plain CRLF-separated",
48+
body: `{"content": "word1\r\nword2\r\n"}`,
49+
want: []string{"word1", "word2"},
50+
},
51+
{
52+
name: "JSON array",
53+
body: `{"content": ["word1", "word2"]}`,
54+
want: []string{"word1", "word2"},
55+
},
56+
}
57+
for _, tt := range tests {
58+
t.Run(tt.name, func(t *testing.T) {
59+
srv := serveContent(t, tt.body)
60+
a := Aspell{RemoteFile: RemoteFile{URL: srv.URL, AllowedItemsKey: "content"}}
61+
got, err := fetchRemoteFile(a)
62+
if err != nil {
63+
t.Fatalf("fetchRemoteFile() error = %v", err)
64+
}
65+
if !slices.Equal(got, tt.want) {
66+
t.Errorf("fetchRemoteFile() = %q, want %q", got, tt.want)
67+
}
68+
})
69+
}
70+
}

0 commit comments

Comments
 (0)