Something went wrong. Try again.
Fork of micro-editor/micro
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494package highlight
import ( "bytes" "errors" "fmt" "regexp"
"gopkg.in/yaml.v2")
// A Group represents a syntax grouptype Group uint8
// Groups contains all of the groups that are defined// You can access them in the map via their string namevar Groups map[string]Groupvar numGroups Group
// String returns the group name attached to the specific groupfunc (g Group) String() string { for k, v := range Groups { if v == g { return k } } return ""}
// A Def is a full syntax definition for a language// It has a filetype, information about how to detect the filetype based// on filename or header (the first line of the file)// Then it has the rules which define how to highlight the filetype Def struct { *Header rules *rules}
type Header struct { FileType string FileNameRegex *regexp.Regexp HeaderRegex *regexp.Regexp SignatureRegex *regexp.Regexp}
type HeaderYaml struct { FileType string `yaml:"filetype"` Detect struct { FNameRegexStr string `yaml:"filename"` HeaderRegexStr string `yaml:"header"` SignatureRegexStr string `yaml:"signature"` } `yaml:"detect"`}
type File struct { FileType string yamlSrc map[any]any}
// A Pattern is one simple syntax rule// It has a group that the rule belongs to, as well as// the regular expression to match the patterntype pattern struct { group Group regex *regexp.Regexp}
// rules defines which patterns and regions can be used to highlight// a filetypetype rules struct { regions []*region patterns []*pattern includes []string}
// A region is a highlighted region (such as a multiline comment, or a string)// It belongs to a group, and has start and end regular expressions// A region also has rules of its own that only apply when matching inside the// region and also rules from the above region do not match inside this region// Note that a region may contain more regionstype region struct { group Group limitGroup Group parent *region start *regexp.Regexp end *regexp.Regexp skip *regexp.Regexp rules *rules}
func init() { Groups = make(map[string]Group)}
// MakeHeader takes a header (.hdr file) file and parses the header// Header files make parsing more efficient when you only want to compute// on the headers of syntax files// A yaml file might take ~400us to parse while a header file only takes ~20usfunc MakeHeader(data []byte) (*Header, error) { lines := bytes.Split(data, []byte{'\n'}) if len(lines) < 4 { return nil, errors.New("Header file has incorrect format") } header := new(Header) var err error header.FileType = string(lines[0]) fnameRegexStr := string(lines[1]) headerRegexStr := string(lines[2]) signatureRegexStr := string(lines[3])
if fnameRegexStr != "" { header.FileNameRegex, err = regexp.Compile(fnameRegexStr) } if err == nil && headerRegexStr != "" { header.HeaderRegex, err = regexp.Compile(headerRegexStr) } if err == nil && signatureRegexStr != "" { header.SignatureRegex, err = regexp.Compile(signatureRegexStr) }
if err != nil { return nil, err }
return header, nil}
// MakeHeaderYaml takes a yaml spec for a syntax file and parses the// headerfunc MakeHeaderYaml(data []byte) (*Header, error) { var hdrYaml HeaderYaml err := yaml.Unmarshal(data, &hdrYaml) if err != nil { return nil, err }
header := new(Header) header.FileType = hdrYaml.FileType
if hdrYaml.Detect.FNameRegexStr != "" { header.FileNameRegex, err = regexp.Compile(hdrYaml.Detect.FNameRegexStr) } if err == nil && hdrYaml.Detect.HeaderRegexStr != "" { header.HeaderRegex, err = regexp.Compile(hdrYaml.Detect.HeaderRegexStr) } if err == nil && hdrYaml.Detect.SignatureRegexStr != "" { header.SignatureRegex, err = regexp.Compile(hdrYaml.Detect.SignatureRegexStr) }
if err != nil { return nil, err }
return header, nil}
// MatchFileName will check the given file name with the stored regexfunc (header *Header) MatchFileName(filename string) bool { if header.FileNameRegex != nil { return header.FileNameRegex.MatchString(filename) }
return false}
func (header *Header) MatchFileHeader(firstLine []byte) bool { if header.HeaderRegex != nil { return header.HeaderRegex.Match(firstLine) }
return false}
// HasFileSignature checks the presence of a stored signaturefunc (header *Header) HasFileSignature() bool { return header.SignatureRegex != nil}
// MatchFileSignature will check the given line with the stored regexfunc (header *Header) MatchFileSignature(line []byte) bool { if header.SignatureRegex != nil { return header.SignatureRegex.Match(line) }
return false}
func ParseFile(input []byte) (f *File, err error) { // This is just so if we have an error, we can exit cleanly and return the parse error to the user defer func() { if r := recover(); r != nil { var ok bool err, ok = r.(error) if !ok { err = fmt.Errorf("pkg: %v", r) } } }()
var rules map[any]any if err = yaml.Unmarshal(input, &rules); err != nil { return nil, err }
f = new(File) f.yamlSrc = rules
for k, v := range rules { if k == "filetype" { filetype := v.(string)
if filetype == "" { return nil, errors.New("empty filetype") }
f.FileType = filetype break } }
if f.FileType == "" { return nil, errors.New("missing filetype") }
return f, err}
// ParseDef parses an input syntax file into a highlight Deffunc ParseDef(f *File, header *Header) (s *Def, err error) { // This is just so if we have an error, we can exit cleanly and return the parse error to the user defer func() { if r := recover(); r != nil { var ok bool err, ok = r.(error) if !ok { err = fmt.Errorf("pkg: %v", r) } } }()
src := f.yamlSrc
s = new(Def) s.Header = header
for k, v := range src { if k == "rules" { inputRules := v.([]any)
rules, err := parseRules(inputRules, nil) if err != nil { return nil, err }
s.rules = rules } }
if s.rules == nil { // allow empty rules s.rules = &rules{} }
return s, err}
// HasIncludes returns whether this syntax def has any include statementsfunc HasIncludes(d *Def) bool { hasIncludes := len(d.rules.includes) > 0 for _, r := range d.rules.regions { hasIncludes = hasIncludes || hasIncludesInRegion(r) } return hasIncludes}
func hasIncludesInRegion(region *region) bool { hasIncludes := len(region.rules.includes) > 0 for _, r := range region.rules.regions { hasIncludes = hasIncludes || hasIncludesInRegion(r) } return hasIncludes}
// GetIncludes returns a list of filetypes that are included by this syntax deffunc GetIncludes(d *Def) []string { includes := d.rules.includes for _, r := range d.rules.regions { includes = append(includes, getIncludesInRegion(r)...) } return includes}
func getIncludesInRegion(region *region) []string { includes := region.rules.includes for _, r := range region.rules.regions { includes = append(includes, getIncludesInRegion(r)...) } return includes}
// ResolveIncludes will sort out the rules for including other filetypes// You should call this after parsing all the Defsfunc ResolveIncludes(def *Def, files []*File) { resolveIncludesInDef(files, def)}
func resolveIncludesInDef(files []*File, d *Def) { for _, lang := range d.rules.includes { for _, searchFile := range files { if lang == searchFile.FileType { searchDef, _ := ParseDef(searchFile, nil) d.rules.patterns = append(d.rules.patterns, searchDef.rules.patterns...) d.rules.regions = append(d.rules.regions, searchDef.rules.regions...) } } } for _, r := range d.rules.regions { resolveIncludesInRegion(files, r) r.parent = nil }}
func resolveIncludesInRegion(files []*File, region *region) { for _, lang := range region.rules.includes { for _, searchFile := range files { if lang == searchFile.FileType { searchDef, _ := ParseDef(searchFile, nil) region.rules.patterns = append(region.rules.patterns, searchDef.rules.patterns...) region.rules.regions = append(region.rules.regions, searchDef.rules.regions...) } } } for _, r := range region.rules.regions { resolveIncludesInRegion(files, r) r.parent = region }}
func parseRules(input []any, curRegion *region) (ru *rules, err error) { defer func() { if r := recover(); r != nil { var ok bool err, ok = r.(error) if !ok { err = fmt.Errorf("pkg: %v", r) } } }() ru = new(rules)
for _, v := range input { rule := v.(map[any]any) for k, val := range rule { group := k
switch object := val.(type) { case string: if object == "" { return nil, fmt.Errorf("Empty rule %s", k) }
if k == "include" { ru.includes = append(ru.includes, object) } else { // Pattern r, err := regexp.Compile(object) if err != nil { return nil, err }
groupStr := group.(string) if _, ok := Groups[groupStr]; !ok { numGroups++ Groups[groupStr] = numGroups } groupNum := Groups[groupStr] ru.patterns = append(ru.patterns, &pattern{groupNum, r}) } case map[any]any: // region region, err := parseRegion(group.(string), object, curRegion) if err != nil { return nil, err } ru.regions = append(ru.regions, region) default: return nil, fmt.Errorf("Bad type %T", object) } } }
return ru, nil}
func parseRegion(group string, regionInfo map[any]any, prevRegion *region) (r *region, err error) { defer func() { if r := recover(); r != nil { var ok bool err, ok = r.(error) if !ok { err = fmt.Errorf("pkg: %v", r) } } }()
r = new(region) if _, ok := Groups[group]; !ok { numGroups++ Groups[group] = numGroups } groupNum := Groups[group] r.group = groupNum r.parent = prevRegion
// start is mandatory if start, ok := regionInfo["start"]; ok { start := start.(string) if start == "" { return nil, fmt.Errorf("Empty start in %s", group) }
r.start, err = regexp.Compile(start) if err != nil { return nil, err } } else { return nil, fmt.Errorf("Missing start in %s", group) }
// end is mandatory if end, ok := regionInfo["end"]; ok { end := end.(string) if end == "" { return nil, fmt.Errorf("Empty end in %s", group) }
r.end, err = regexp.Compile(end) if err != nil { return nil, err } } else { return nil, fmt.Errorf("Missing end in %s", group) }
// skip is optional if skip, ok := regionInfo["skip"]; ok { skip := skip.(string) if skip == "" { return nil, fmt.Errorf("Empty skip in %s", group) }
r.skip, err = regexp.Compile(skip) if err != nil { return nil, err } }
// limit-color is optional if groupStr, ok := regionInfo["limit-group"]; ok { groupStr := groupStr.(string) if groupStr == "" { return nil, fmt.Errorf("Empty limit-group in %s", group) }
if _, ok := Groups[groupStr]; !ok { numGroups++ Groups[groupStr] = numGroups } groupNum := Groups[groupStr] r.limitGroup = groupNum
if err != nil { return nil, err } } else { r.limitGroup = r.group }
// rules are optional if rules, ok := regionInfo["rules"]; ok { r.rules, err = parseRules(rules.([]any), r) if err != nil { return nil, err } }
if r.rules == nil { // allow empty rules r.rules = &rules{} }
return r, nil}