1package main
2
3import (
4	"bytes"
5	"crypto/md5"
6	"encoding/hex"
7	"encoding/json"
8	"fmt"
9	"go/ast"
10	"go/token"
11	"io"
12	"io/fs"
13	"io/ioutil"
14	"log"
15	"os"
16	"os/exec"
17	"path/filepath"
18	"regexp"
19	"sort"
20	"strings"
21
22	"golang.org/x/tools/go/packages"
23)
24
25const header = `
26// This file is autogenerated.
27package localescompressed
28`
29
30func main() {
31	createTranslatorsMap()
32	createCurrenciesMap()
33}
34
35func createTranslatorsMap() {
36	const localeMod = "github.com/gohugoio/locales"
37	b := &bytes.Buffer{}
38	cmd := exec.Command("go", "list", "-m", "-json", localeMod)
39	cmd.Stdout = b
40
41	if err := cmd.Run(); err != nil {
42		log.Fatal(err)
43	}
44
45	m := make(map[string]interface{})
46	if err := json.Unmarshal(b.Bytes(), &m); err != nil {
47		log.Fatal(err)
48	}
49
50	dir := m["Dir"].(string)
51
52	var packageNames []string
53
54	filepath.WalkDir(dir, func(path string, d fs.DirEntry, err error) error {
55		if !d.IsDir() || d.Name() == "currency" {
56			return nil
57		}
58
59		name := filepath.Base(path)
60
61		if _, err := os.Stat(filepath.Join(path, fmt.Sprintf("%s.go", name))); err == nil {
62			packageNames = append(packageNames, name)
63		}
64
65		return nil
66	})
67
68	sort.Strings(packageNames)
69
70	cfg := &packages.Config{Mode: packages.NeedSyntax | packages.NeedFiles}
71	f, err := os.Create(filepath.Join("..", "locales.autogen.go"))
72	if err != nil {
73		log.Fatal(err)
74	}
75	defer f.Close()
76
77	fmt.Fprint(f, header)
78	fmt.Fprintf(f, "import(\n\"math\"\n\"time\"\n\"strconv\"\n\"github.com/gohugoio/locales\"\n\"github.com/gohugoio/locales/currency\"\n)\n\n")
79
80	var (
81		allFields []string
82		methodSet = make(map[string]bool)
83		initW     = &bytes.Buffer{}
84		counter   = 0
85	)
86
87	for _, k := range packageNames {
88		counter++
89
90		if counter%50 == 0 {
91			fmt.Printf("[%d] Handling locale %s ...\n", counter, k)
92		}
93
94		pkgs, err := packages.Load(cfg, fmt.Sprintf("github.com/gohugoio/locales/%s", k))
95		if err != nil {
96			log.Fatal(err)
97		}
98
99		pkg := pkgs[0]
100		gf := pkg.GoFiles[0]
101
102		src, err := ioutil.ReadFile(gf)
103		if err != nil {
104			log.Fatal(err)
105		}
106
107		collector := &coll{src: src, locale: k, w: f, initW: initW, methodSet: methodSet}
108
109		for _, f := range pkg.Syntax {
110			for _, node := range f.Decls {
111				ast.Inspect(node, collector.collectFields)
112			}
113		}
114
115		collector.fields = uniqueStringsSorted(collector.fields)
116		collector.methods = uniqueStringsSorted(collector.methods)
117		allFields = append(allFields, collector.fields...)
118		allFields = uniqueStringsSorted(allFields)
119
120		for _, f := range pkg.Syntax {
121			for _, node := range f.Decls {
122				ast.Inspect(node, collector.collectNew)
123			}
124		}
125
126	}
127
128	fmt.Fprint(f, "\ntype localen struct {\n")
129	for _, field := range allFields {
130		fmt.Fprintf(f, "\t%s\n", field)
131	}
132
133	fmt.Fprint(f, "\n}\n")
134
135	fmt.Fprint(f, "func init() {\n")
136	fmt.Fprint(f, initW.String())
137	fmt.Fprint(f, "\n}")
138}
139
140type coll struct {
141	// Global
142	w         io.Writer
143	initW     io.Writer
144	methodSet map[string]bool
145
146	// Local
147	locale  string
148	fields  []string
149	methods []string
150	src     []byte
151}
152
153func (c *coll) collectFields(node ast.Node) bool {
154	switch vv := node.(type) {
155
156	case *ast.FuncDecl:
157		if vv.Recv != nil {
158			recName := vv.Recv.List[0].Names[0].Name
159			name := vv.Name.Name
160
161			start := vv.Pos() - 1
162			end := vv.End()
163
164			body := string(c.src[start : end-1])
165			re := regexp.MustCompile(`\b` + recName + `\.`)
166			body = re.ReplaceAllString(body, " ln.")
167			body = body[strings.Index(body, name):]
168			hash := toMd5(body)
169			body = body[len(name)+1:]
170			fullName := name + "_" + hash
171
172			sig := body[:strings.Index(body, "{")]
173			funcSig := "func(ln *localen"
174			if !strings.HasPrefix(sig, ")") {
175				funcSig += ", "
176			}
177			field := fmt.Sprintf("fn%s %s%s", name, funcSig, sig)
178			method := fmt.Sprintf("fn%s fn%s", name, fullName)
179
180			c.methods = append(c.methods, method)
181			c.fields = append(c.fields, field)
182
183			body = "var fn" + fullName + " = " + funcSig + body + "\n"
184
185			if !c.methodSet[hash] {
186				fmt.Fprint(c.w, body)
187			}
188			c.methodSet[hash] = true
189		}
190	case *ast.GenDecl:
191		for _, spec := range vv.Specs {
192			switch spec.(type) {
193			case *ast.TypeSpec:
194				typeSpec := spec.(*ast.TypeSpec)
195				switch typeSpec.Type.(type) {
196				case *ast.StructType:
197					structType := typeSpec.Type.(*ast.StructType)
198					for _, field := range structType.Fields.List {
199						typeExpr := field.Type
200
201						start := typeExpr.Pos() - 1
202						end := typeExpr.End() - 1
203
204						typeInSource := c.src[start:end]
205
206						c.fields = append(c.fields, fmt.Sprintf("%s %s", field.Names[0].Name, string(typeInSource)))
207					}
208				}
209			}
210		}
211
212	}
213
214	return false
215}
216
217var returnStructRe = regexp.MustCompile(`return &.*{`)
218
219func (c *coll) collectNew(node ast.Node) bool {
220	switch vv := node.(type) {
221	case *ast.FuncDecl:
222		if vv.Name.Name == "New" {
223			start := vv.Body.Pos() - 1
224			end := vv.Body.End() - 1
225
226			body := string(c.src[start : end-1])
227
228			body = strings.Replace(body, "{\n\t", "", 1)
229			body = strings.Replace(body, "\t}", "", 1)
230
231			body = returnStructRe.ReplaceAllString(body, "")
232
233			for _, method := range c.methods {
234				if strings.HasPrefix(method, "fn") {
235					parts := strings.Fields(method)
236					body += fmt.Sprintf("\n%s: %s,", parts[0], parts[1])
237				}
238			}
239			fmt.Fprintf(c.initW, "\ttranslatorFuncs[%q] = %s\n", strings.ToLower(c.locale), fmt.Sprintf("func() locales.Translator {\nreturn &localen{\n%s\n}\n}", body))
240		}
241	}
242	return false
243}
244
245func uniqueStringsSorted(s []string) []string {
246	if len(s) == 0 {
247		return nil
248	}
249	ss := sort.StringSlice(s)
250	ss.Sort()
251	i := 0
252	for j := 1; j < len(s); j++ {
253		if !ss.Less(i, j) {
254			continue
255		}
256		i++
257		s[i] = s[j]
258	}
259
260	return s[:i+1]
261}
262
263func toMd5(f string) string {
264	h := md5.New()
265	h.Write([]byte(f))
266	return hex.EncodeToString(h.Sum([]byte{}))
267}
268
269func createCurrenciesMap() {
270	cfg := &packages.Config{
271		Mode:  packages.LoadSyntax,
272		Tests: false,
273	}
274
275	pkgs, err := packages.Load(cfg, "github.com/gohugoio/locales/currency")
276	if err != nil {
277		log.Fatal(err)
278	}
279
280	pkg := pkgs[0]
281
282	collector := &currencyCollector{}
283
284	for _, f := range pkg.Syntax {
285		ast.Inspect(f, collector.handleNode)
286	}
287
288	sort.Strings(collector.constants)
289
290	f, err := os.Create(filepath.Join("../currencies.autogen.go"))
291	if err != nil {
292		log.Fatal(err)
293	}
294	defer f.Close()
295
296	fmt.Fprintf(f, "%s\nimport \"github.com/gohugoio/locales/currency\"\n", header)
297
298	fmt.Fprintf(f, "var currencies = map[string]currency.Type {")
299	for _, currency := range collector.constants {
300		fmt.Fprintf(f, "\n%q: currency.%s,", currency, currency)
301	}
302	fmt.Fprintln(f, "}")
303}
304
305type currencyCollector struct {
306	constants []string
307}
308
309func (c *currencyCollector) handleNode(node ast.Node) bool {
310	decl, ok := node.(*ast.GenDecl)
311	if !ok || decl.Tok != token.CONST {
312		return true
313	}
314	typ := ""
315	for _, spec := range decl.Specs {
316		vspec := spec.(*ast.ValueSpec)
317		if vspec.Type == nil && len(vspec.Values) > 0 {
318			typ = ""
319
320			ce, ok := vspec.Values[0].(*ast.CallExpr)
321			if !ok {
322				continue
323			}
324			id, ok := ce.Fun.(*ast.Ident)
325			if !ok {
326				continue
327			}
328			typ = id.Name
329		}
330		if vspec.Type != nil {
331			ident, ok := vspec.Type.(*ast.Ident)
332			if !ok {
333				continue
334			}
335			typ = ident.Name
336		}
337		if typ != "Type" {
338			// This is not the type we're looking for.
339			continue
340		}
341		// We now have a list of names (from one line of source code) all being
342		// declared with the desired type.
343		// Grab their names and actual values and store them in f.values.
344		for _, name := range vspec.Names {
345			c.constants = append(c.constants, name.String())
346		}
347	}
348	return false
349}
350