1package spellcheck
2
3import (
4 "context"
5 "fmt"
6 "io"
7 "net/http"
8 "os"
9 "path/filepath"
10 "strings"
11
12 "github.com/floatpane/matcha/internal/httpclient"
13)
14
15// DefaultLanguage is the language code installed automatically the first
16// time the composer opens.
17const DefaultLanguage = "en"
18
19// DictURLTemplate is the URL used to fetch Hunspell .dic files. It is a
20// variable to allow tests and the CLI to override the source.
21var DictURLTemplate = "https://raw.githubusercontent.com/wooorm/dictionaries/main/dictionaries/%s/index.dic"
22
23// Download fetches the dictionary for lang from DictURLTemplate and writes
24// it atomically to the dicts directory.
25func Download(lang string) (string, error) {
26 if lang == "" {
27 return "", fmt.Errorf("empty language code")
28 }
29 dest, err := DictPath(lang)
30 if err != nil {
31 return "", err
32 }
33
34 url := fmt.Sprintf(DictURLTemplate, urlPathLang(lang))
35 client := httpclient.New(httpclient.InstallTimeout)
36 req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, url, nil)
37 if err != nil {
38 return "", fmt.Errorf("build request: %w", err)
39 }
40 resp, err := client.Do(req)
41 if err != nil {
42 return "", fmt.Errorf("download %s: %w", lang, err)
43 }
44 defer resp.Body.Close() //nolint:errcheck
45
46 if resp.StatusCode != http.StatusOK {
47 return "", fmt.Errorf("download %s: status %d", lang, resp.StatusCode)
48 }
49
50 tmp, err := os.CreateTemp(filepath.Dir(dest), ".dl-*")
51 if err != nil {
52 return "", fmt.Errorf("create temp: %w", err)
53 }
54 tmpPath := tmp.Name()
55 defer os.Remove(tmpPath) //nolint:errcheck
56
57 if _, err := io.Copy(tmp, resp.Body); err != nil {
58 tmp.Close() //nolint:errcheck,gosec
59 return "", fmt.Errorf("write dict: %w", err)
60 }
61 if err := tmp.Close(); err != nil {
62 return "", fmt.Errorf("close dict: %w", err)
63 }
64 if err := os.Rename(tmpPath, dest); err != nil {
65 return "", fmt.Errorf("install dict: %w", err)
66 }
67 return dest, nil
68}
69
70// EnsureDefault downloads the default English dictionary if it is not
71// already installed and returns the language code that is available.
72func EnsureDefault() (string, error) {
73 if DictInstalled(DefaultLanguage) {
74 return DefaultLanguage, nil
75 }
76 _, err := Download(DefaultLanguage)
77 if err != nil {
78 return "", err
79 }
80 return DefaultLanguage, nil
81}
82
83// urlPathLang converts a language code into the directory name used by the
84// wooorm/dictionaries repository ("en", "en-GB", "de", ...). The code is
85// passed through after normalising the region separator.
86func urlPathLang(lang string) string {
87 lang = strings.TrimSpace(lang)
88 lang = strings.ReplaceAll(lang, "_", "-")
89 return lang
90}