]> git.lizzy.rs Git - micro.git/blob - pkg/highlight/parser.go
Merge pull request #1437 from serebit/patch-2
[micro.git] / pkg / highlight / parser.go
1 package highlight
2
3 import (
4         "bytes"
5         "errors"
6         "fmt"
7         "regexp"
8
9         "gopkg.in/yaml.v2"
10 )
11
12 // A Group represents a syntax group
13 type Group uint8
14
15 // Groups contains all of the groups that are defined
16 // You can access them in the map via their string name
17 var Groups map[string]Group
18 var numGroups Group
19
20 // String returns the group name attached to the specific group
21 func (g Group) String() string {
22         for k, v := range Groups {
23                 if v == g {
24                         return k
25                 }
26         }
27         return ""
28 }
29
30 // A Def is a full syntax definition for a language
31 // It has a filetype, information about how to detect the filetype based
32 // on filename or header (the first line of the file)
33 // Then it has the rules which define how to highlight the file
34 type Def struct {
35         *Header
36
37         rules *rules
38 }
39
40 type Header struct {
41         FileType string
42         FtDetect [2]*regexp.Regexp
43 }
44
45 type HeaderYaml struct {
46         FileType string `yaml:"filetype"`
47         Detect   struct {
48                 FNameRgx  string `yaml:"filename"`
49                 HeaderRgx string `yaml:"header"`
50         } `yaml:"detect"`
51 }
52
53 type File struct {
54         FileType string
55
56         yamlSrc map[interface{}]interface{}
57 }
58
59 // A Pattern is one simple syntax rule
60 // It has a group that the rule belongs to, as well as
61 // the regular expression to match the pattern
62 type pattern struct {
63         group Group
64         regex *regexp.Regexp
65 }
66
67 // rules defines which patterns and regions can be used to highlight
68 // a filetype
69 type rules struct {
70         regions  []*region
71         patterns []*pattern
72         includes []string
73 }
74
75 // A region is a highlighted region (such as a multiline comment, or a string)
76 // It belongs to a group, and has start and end regular expressions
77 // A region also has rules of its own that only apply when matching inside the
78 // region and also rules from the above region do not match inside this region
79 // Note that a region may contain more regions
80 type region struct {
81         group      Group
82         limitGroup Group
83         parent     *region
84         start      *regexp.Regexp
85         end        *regexp.Regexp
86         skip       *regexp.Regexp
87         rules      *rules
88 }
89
90 func init() {
91         Groups = make(map[string]Group)
92 }
93
94 // MakeHeader takes a header (.hdr file) file and parses the header
95 // Header files make parsing more efficient when you only want to compute
96 // on the headers of syntax files
97 // A yaml file might take ~400us to parse while a header file only takes ~20us
98 func MakeHeader(data []byte) (*Header, error) {
99         lines := bytes.Split(data, []byte{'\n'})
100         if len(lines) < 3 {
101                 return nil, errors.New("Header file has incorrect format")
102         }
103         header := new(Header)
104         var err error
105         header.FileType = string(lines[0])
106         fnameRgx := string(lines[1])
107         headerRgx := string(lines[2])
108
109         if fnameRgx != "" {
110                 header.FtDetect[0], err = regexp.Compile(fnameRgx)
111         }
112         if headerRgx != "" {
113                 header.FtDetect[1], err = regexp.Compile(headerRgx)
114         }
115
116         if err != nil {
117                 return nil, err
118         }
119
120         return header, nil
121 }
122
123 // MakeHeaderYaml takes a yaml spec for a syntax file and parses the
124 // header
125 func MakeHeaderYaml(data []byte) (*Header, error) {
126         var hdrYaml HeaderYaml
127         err := yaml.Unmarshal(data, &hdrYaml)
128         if err != nil {
129                 return nil, err
130         }
131
132         header := new(Header)
133         header.FileType = hdrYaml.FileType
134
135         if hdrYaml.Detect.FNameRgx != "" {
136                 header.FtDetect[0], err = regexp.Compile(hdrYaml.Detect.FNameRgx)
137         }
138         if hdrYaml.Detect.HeaderRgx != "" {
139                 header.FtDetect[1], err = regexp.Compile(hdrYaml.Detect.HeaderRgx)
140         }
141
142         if err != nil {
143                 return nil, err
144         }
145
146         return header, nil
147 }
148
149 func ParseFile(input []byte) (f *File, err error) {
150         // This is just so if we have an error, we can exit cleanly and return the parse error to the user
151         defer func() {
152                 if r := recover(); r != nil {
153                         var ok bool
154                         err, ok = r.(error)
155                         if !ok {
156                                 err = fmt.Errorf("pkg: %v", r)
157                         }
158                 }
159         }()
160
161         var rules map[interface{}]interface{}
162         if err = yaml.Unmarshal(input, &rules); err != nil {
163                 return nil, err
164         }
165
166         f = new(File)
167         f.yamlSrc = rules
168
169         for k, v := range rules {
170                 if k == "filetype" {
171                         filetype := v.(string)
172
173                         f.FileType = filetype
174                         break
175                 }
176         }
177
178         return f, err
179 }
180
181 // ParseDef parses an input syntax file into a highlight Def
182 func ParseDef(f *File, header *Header) (s *Def, err error) {
183         // This is just so if we have an error, we can exit cleanly and return the parse error to the user
184         defer func() {
185                 if r := recover(); r != nil {
186                         var ok bool
187                         err, ok = r.(error)
188                         if !ok {
189                                 err = fmt.Errorf("pkg: %v", r)
190                         }
191                 }
192         }()
193
194         rules := f.yamlSrc
195
196         s = new(Def)
197         s.Header = header
198
199         for k, v := range rules {
200                 if k == "rules" {
201                         inputRules := v.([]interface{})
202
203                         rules, err := parseRules(inputRules, nil)
204                         if err != nil {
205                                 return nil, err
206                         }
207
208                         s.rules = rules
209                 }
210         }
211
212         return s, err
213 }
214
215 // HasIncludes returns whether this syntax def has any include statements
216 func HasIncludes(d *Def) bool {
217         hasIncludes := len(d.rules.includes) > 0
218         for _, r := range d.rules.regions {
219                 hasIncludes = hasIncludes || hasIncludesInRegion(r)
220         }
221         return hasIncludes
222 }
223
224 func hasIncludesInRegion(region *region) bool {
225         hasIncludes := len(region.rules.includes) > 0
226         for _, r := range region.rules.regions {
227                 hasIncludes = hasIncludes || hasIncludesInRegion(r)
228         }
229         return hasIncludes
230 }
231
232 // GetIncludes returns a list of filetypes that are included by this syntax def
233 func GetIncludes(d *Def) []string {
234         includes := d.rules.includes
235         for _, r := range d.rules.regions {
236                 includes = append(includes, getIncludesInRegion(r)...)
237         }
238         return includes
239 }
240
241 func getIncludesInRegion(region *region) []string {
242         includes := region.rules.includes
243         for _, r := range region.rules.regions {
244                 includes = append(includes, getIncludesInRegion(r)...)
245         }
246         return includes
247 }
248
249 // ResolveIncludes will sort out the rules for including other filetypes
250 // You should call this after parsing all the Defs
251 func ResolveIncludes(def *Def, files []*File) {
252         resolveIncludesInDef(files, def)
253 }
254
255 func resolveIncludesInDef(files []*File, d *Def) {
256         for _, lang := range d.rules.includes {
257                 for _, searchFile := range files {
258                         if lang == searchFile.FileType {
259                                 searchDef, _ := ParseDef(searchFile, nil)
260                                 d.rules.patterns = append(d.rules.patterns, searchDef.rules.patterns...)
261                                 d.rules.regions = append(d.rules.regions, searchDef.rules.regions...)
262                         }
263                 }
264         }
265         for _, r := range d.rules.regions {
266                 resolveIncludesInRegion(files, r)
267                 r.parent = nil
268         }
269 }
270
271 func resolveIncludesInRegion(files []*File, region *region) {
272         for _, lang := range region.rules.includes {
273                 for _, searchFile := range files {
274                         if lang == searchFile.FileType {
275                                 searchDef, _ := ParseDef(searchFile, nil)
276                                 region.rules.patterns = append(region.rules.patterns, searchDef.rules.patterns...)
277                                 region.rules.regions = append(region.rules.regions, searchDef.rules.regions...)
278                         }
279                 }
280         }
281         for _, r := range region.rules.regions {
282                 resolveIncludesInRegion(files, r)
283                 r.parent = region
284         }
285 }
286
287 func parseRules(input []interface{}, curRegion *region) (ru *rules, err error) {
288         defer func() {
289                 if r := recover(); r != nil {
290                         var ok bool
291                         err, ok = r.(error)
292                         if !ok {
293                                 err = fmt.Errorf("pkg: %v", r)
294                         }
295                 }
296         }()
297         ru = new(rules)
298
299         for _, v := range input {
300                 rule := v.(map[interface{}]interface{})
301                 for k, val := range rule {
302                         group := k
303
304                         switch object := val.(type) {
305                         case string:
306                                 if k == "include" {
307                                         ru.includes = append(ru.includes, object)
308                                 } else {
309                                         // Pattern
310                                         r, err := regexp.Compile(object)
311                                         if err != nil {
312                                                 return nil, err
313                                         }
314
315                                         groupStr := group.(string)
316                                         if _, ok := Groups[groupStr]; !ok {
317                                                 numGroups++
318                                                 Groups[groupStr] = numGroups
319                                         }
320                                         groupNum := Groups[groupStr]
321                                         ru.patterns = append(ru.patterns, &pattern{groupNum, r})
322                                 }
323                         case map[interface{}]interface{}:
324                                 // region
325                                 region, err := parseRegion(group.(string), object, curRegion)
326                                 if err != nil {
327                                         return nil, err
328                                 }
329                                 ru.regions = append(ru.regions, region)
330                         default:
331                                 return nil, fmt.Errorf("Bad type %T", object)
332                         }
333                 }
334         }
335
336         return ru, nil
337 }
338
339 func parseRegion(group string, regionInfo map[interface{}]interface{}, prevRegion *region) (r *region, err error) {
340         defer func() {
341                 if r := recover(); r != nil {
342                         var ok bool
343                         err, ok = r.(error)
344                         if !ok {
345                                 err = fmt.Errorf("pkg: %v", r)
346                         }
347                 }
348         }()
349
350         r = new(region)
351         if _, ok := Groups[group]; !ok {
352                 numGroups++
353                 Groups[group] = numGroups
354         }
355         groupNum := Groups[group]
356         r.group = groupNum
357         r.parent = prevRegion
358
359         r.start, err = regexp.Compile(regionInfo["start"].(string))
360
361         if err != nil {
362                 return nil, err
363         }
364
365         r.end, err = regexp.Compile(regionInfo["end"].(string))
366
367         if err != nil {
368                 return nil, err
369         }
370
371         // skip is optional
372         if _, ok := regionInfo["skip"]; ok {
373                 r.skip, err = regexp.Compile(regionInfo["skip"].(string))
374
375                 if err != nil {
376                         return nil, err
377                 }
378         }
379
380         // limit-color is optional
381         if _, ok := regionInfo["limit-group"]; ok {
382                 groupStr := regionInfo["limit-group"].(string)
383                 if _, ok := Groups[groupStr]; !ok {
384                         numGroups++
385                         Groups[groupStr] = numGroups
386                 }
387                 groupNum := Groups[groupStr]
388                 r.limitGroup = groupNum
389
390                 if err != nil {
391                         return nil, err
392                 }
393         } else {
394                 r.limitGroup = r.group
395         }
396
397         r.rules, err = parseRules(regionInfo["rules"].([]interface{}), r)
398
399         if err != nil {
400                 return nil, err
401         }
402
403         return r, nil
404 }