internal/generator/main.go (view raw)
1package main
2
3import (
4 "bufio"
5 "bytes"
6 "encoding/json"
7 "fmt"
8 "go/format"
9 "io"
10 "io/ioutil"
11 "net/http"
12 "os"
13 "sort"
14 "strings"
15 "text/template"
16 "time"
17 "unicode"
18)
19
20const (
21 emojiListUrl = "https://unicode.org/Public/emoji/13.0/emoji-test.txt"
22 gemojiURL = "https://raw.githubusercontent.com/github/gemoji/master/db/emoji.json"
23 constantsFile = "constants.go"
24 aliasesFile = "map.go"
25)
26
27// unicode and gemoji databases don't have alias like that
28var customEmojis = map[string]string{
29 ":robot_face:": "\U0001f916", // slack
30}
31
32func main() {
33 emojis, err := fetch()
34 if err != nil {
35 panic(err)
36 }
37
38 constants := generateConstants(emojis)
39 aliases := generateAliases(emojis)
40
41 if err = save(constantsFile, constants); err != nil {
42 panic(err)
43 }
44
45 if err = save(aliasesFile, aliases); err != nil {
46 panic(err)
47 }
48}
49
50func fetch() (*groups, error) {
51 var emojis groups
52 b, err := fetchData(emojiListUrl)
53 if err != nil {
54 return nil, err
55 }
56
57 var grp *group
58 var subgrp *subgroup
59
60 parseLine := func(line string) {
61 switch {
62 case strings.HasPrefix(line, "# group:"):
63 name := strings.TrimSpace(strings.ReplaceAll(line, "# group:", ""))
64 grp = emojis.Append(name)
65 case strings.HasPrefix(line, "# subgroup:"):
66 name := strings.TrimSpace(strings.ReplaceAll(line, "# subgroup:", ""))
67 subgrp = grp.Append(name)
68 case !strings.HasPrefix(line, "#"):
69 if e := newEmoji(line); e != nil {
70 subgrp.Append(*e)
71 }
72 }
73 }
74
75 if err = readLines(b, parseLine); err != nil {
76 return nil, err
77 }
78
79 return &emojis, nil
80}
81
82func generateAliases(emojis *groups) string {
83 var aliases []string
84 var emojiMap = make(map[string]string)
85
86 for _, grp := range emojis.Groups {
87 for _, subgrp := range grp.Subgroups {
88 for _, c := range subgrp.Constants {
89 emoji := subgrp.Emojis[c][0]
90 alias := ":" + snakeCase(emoji.Constant) + ":"
91 aliases = append(aliases, alias)
92 emojiMap[alias] = emoji.Code
93 }
94 }
95 }
96
97 // add gemoji aliases
98 {
99 gemojis, err := fetchGemoji()
100 if err != nil {
101 panic(err)
102 }
103
104 for alias, code := range gemojis {
105 _, ok := emojiMap[alias]
106 if !ok {
107 aliases = append(aliases, alias)
108 }
109 emojiMap[alias] = code
110 }
111 }
112
113 // add custom emoji aliases
114 {
115 for alias, code := range customEmojis {
116 _, ok := emojiMap[alias]
117 if !ok {
118 aliases = append(aliases, alias)
119 }
120 emojiMap[alias] = code
121 }
122 }
123
124 var res string
125 sort.Strings(aliases)
126 for _, alias := range aliases {
127 res += fmt.Sprintf("%q: %+q,\n", alias, emojiMap[alias])
128 }
129
130 return res
131}
132
133func snakeCase(str string) string {
134 var output strings.Builder
135 for i, r := range str {
136 switch {
137 case unicode.IsUpper(r):
138 if i != 0 {
139 output.WriteRune('_')
140 }
141 output.WriteRune(unicode.ToLower(r))
142 case unicode.IsDigit(r):
143 if i != 0 && !unicode.IsDigit(rune(str[i-1])) {
144 output.WriteRune('_')
145 }
146 output.WriteRune(r)
147 default:
148 output.WriteRune(r)
149 }
150 }
151
152 return output.String()
153}
154
155type gemoji struct {
156 Emoji string `json:"emoji"`
157 Aliases []string `json:"aliases"`
158}
159
160func fetchGemoji() (map[string]string, error) {
161 b, err := fetchData(gemojiURL)
162 if err != nil {
163 return nil, err
164 }
165
166 var gemojis []gemoji
167 r := make(map[string]string)
168
169 if err = json.Unmarshal(b, &gemojis); err != nil {
170 return nil, err
171 }
172
173 for _, gemoji := range gemojis {
174 for _, alias := range gemoji.Aliases {
175 if len(alias) == 0 || len(gemoji.Emoji) == 0 {
176 continue
177 }
178
179 r[":"+alias+":"] = gemoji.Emoji
180 }
181 }
182
183 return r, nil
184}
185
186func generateConstants(emojis *groups) string {
187 var res string
188 for _, grp := range emojis.Groups {
189 res += fmt.Sprintf("\n// GROUP: %v\n", grp.Name)
190 for _, subgrp := range grp.Subgroups {
191 res += fmt.Sprintf("// SUBGROUP: %v\n", subgrp.Name)
192 for _, c := range subgrp.Constants {
193 res += emojiConstant(subgrp.Emojis[c])
194 }
195 }
196 }
197
198 return res
199}
200
201func emojiConstant(emojis []emoji) string {
202 basic := emojis[0]
203 switch len(emojis) {
204 case 1:
205 return fmt.Sprintf("%s Emoji = %+q // %s\n", basic.Constant, basic.Code, basic.Name)
206 case 6:
207 oneTonedCode := replaceTones(emojis[1].Code)
208 defaultTone := defaultTone(basic.Code, oneTonedCode)
209
210 if defaultTone != "" {
211 return fmt.Sprintf("%s EmojiWithTone = newEmojiWithTone(%+q).withDefaultTone(%+q) // %s\n",
212 basic.Constant, oneTonedCode, defaultTone, basic.Name)
213 }
214
215 return fmt.Sprintf("%s EmojiWithTone = newEmojiWithTone(%+q) // %s\n",
216 basic.Constant, oneTonedCode, basic.Name)
217 case 26:
218 oneTonedCode := replaceTones(emojis[1].Code)
219 twoTonedCode := replaceTones(emojis[2].Code)
220
221 return fmt.Sprintf("%s EmojiWithTone = newEmojiWithTone(%+q, %+q) // %s\n",
222 basic.Constant, oneTonedCode, twoTonedCode, basic.Name)
223 default:
224 panic(fmt.Errorf("not expected emoji count for a constant: %v", len(emojis)))
225 }
226}
227
228func save(filename, data string) error {
229 tmpl, err := template.ParseFiles(fmt.Sprintf("internal/generator/%v.tmpl", filename))
230 if err != nil {
231 return err
232 }
233
234 d := struct {
235 Link string
236 Date string
237 Data string
238 }{
239 Link: emojiListUrl,
240 Date: time.Now().Format(time.RFC3339),
241 Data: data,
242 }
243
244 var w bytes.Buffer
245 if err = tmpl.Execute(&w, d); err != nil {
246 return err
247 }
248
249 content, err := format.Source(w.Bytes())
250
251 file, err := os.Create(filename)
252 if err != nil {
253 return fmt.Errorf("could not create file: %v", err)
254 }
255 defer file.Close()
256
257 if _, err := file.Write(content); err != nil {
258 return fmt.Errorf("could not write to file: %v", err)
259 }
260 return nil
261}
262
263func fetchData(url string) ([]byte, error) {
264 resp, err := http.Get(url)
265 if err != nil {
266 return nil, err
267 }
268 defer resp.Body.Close()
269
270 // Check server response
271 if resp.StatusCode != http.StatusOK {
272 return nil, fmt.Errorf("bad status: %s", resp.Status)
273 }
274
275 b, err := ioutil.ReadAll(resp.Body)
276 if err != nil {
277 return nil, err
278 }
279
280 return b, nil
281}
282
283func readLines(b []byte, fn func(string)) error {
284 reader := bufio.NewReader(bytes.NewReader(b))
285
286 var line string
287 var err error
288 for {
289 line, err = reader.ReadString('\n')
290 if err != nil {
291 break
292 }
293
294 fn(line)
295 }
296
297 if err != io.EOF {
298 return err
299 }
300
301 return nil
302}