commit 5a939241ee8f8dcc5746ac588126c297cf763cbf Author: Zachary Yedidia Date: Sun Feb 12 11:21:27 2017 -0500 Initial commit diff --git a/ftdetect.go b/ftdetect.go new file mode 100644 index 0000000..d4f0070 --- /dev/null +++ b/ftdetect.go @@ -0,0 +1,18 @@ +package highlight + +import "bytes" + +func DetectFiletype(defs []*Def, filename string, fileSrc []byte) string { + firstLine := bytes.Split(fileSrc, []byte("\n"))[0] + + for _, d := range defs { + if d.ftdetect[0].Match([]byte(filename)) { + return d.ft + } + if d.ftdetect[1].Match(firstLine) { + return d.ft + } + } + + return "Unknown" +} diff --git a/highlighter.go b/highlighter.go new file mode 100644 index 0000000..cca6f44 --- /dev/null +++ b/highlighter.go @@ -0,0 +1,153 @@ +package highlight + +import ( + "regexp" + "strings" +) + +func combineLineMatch(src, dst LineMatch) LineMatch { + for k, v := range src { + dst[k] = v + } + return dst +} + +type Highlighter struct { + states []*Region + def *Def +} + +func NewHighlighter(def *Def) *Highlighter { + h := new(Highlighter) + h.def = def + return h +} + +type LineMatch map[int]string + +func FindIndex(regex *regexp.Regexp, str []byte, canMatchStart, canMatchEnd bool) []int { + regexStr := regex.String() + if strings.Contains(regexStr, "^") { + if !canMatchStart { + return nil + } + } + if strings.Contains(regexStr, "$") { + if !canMatchEnd { + return nil + } + } + return regex.FindIndex(str) +} + +func FindAllIndex(regex *regexp.Regexp, str []byte, canMatchStart, canMatchEnd bool) [][]int { + regexStr := regex.String() + if strings.Contains(regexStr, "^") { + if !canMatchStart { + return nil + } + } + if strings.Contains(regexStr, "$") { + if !canMatchEnd { + return nil + } + } + return regex.FindAllIndex(str, -1) +} + +func (h *Highlighter) highlightRegion(start int, canMatchEnd bool, lineNum int, line []byte, region *Region) LineMatch { + highlights := make(LineMatch) + + if len(line) == 0 { + if canMatchEnd { + h.states[lineNum] = region + } + + return highlights + } + + loc := FindIndex(region.end, line, start == 0, canMatchEnd) + if loc != nil { + if region.parent == nil { + highlights[start+loc[1]] = "" + return combineLineMatch(highlights, combineLineMatch(h.highlightRegion(start, false, lineNum, line[:loc[0]], region), h.highlightEmptyRegion(start+loc[1], canMatchEnd, lineNum, line[loc[1]:]))) + } else { + highlights[start+loc[1]] = region.parent.group + return combineLineMatch(highlights, combineLineMatch(h.highlightRegion(start, false, lineNum, line[:loc[0]], region), h.highlightRegion(start+loc[1], canMatchEnd, lineNum, line[loc[1]:], region.parent))) + } + } + + for _, r := range region.rules.regions { + loc = FindIndex(r.start, line, start == 0, canMatchEnd) + if loc != nil { + highlights[start+loc[0]] = r.group + return combineLineMatch(highlights, combineLineMatch(h.highlightRegion(start, false, lineNum, line[:loc[0]], region), h.highlightRegion(start+loc[1], canMatchEnd, lineNum, line[loc[1]:], r))) + } + } + + for _, p := range region.rules.patterns { + matches := FindAllIndex(p.regex, line, start == 0, canMatchEnd) + for _, m := range matches { + highlights[start+m[0]] = p.group + if _, ok := highlights[start+m[1]]; !ok { + highlights[start+m[1]] = region.group + } + } + } + + if canMatchEnd { + h.states[lineNum] = region + } + + return highlights +} + +func (h *Highlighter) highlightEmptyRegion(start int, canMatchEnd bool, lineNum int, line []byte) LineMatch { + highlights := make(LineMatch) + if len(line) == 0 { + if canMatchEnd { + h.states[lineNum] = nil + } + return highlights + } + + for _, r := range h.def.rules.regions { + loc := FindIndex(r.start, line, start == 0, canMatchEnd) + if loc != nil { + highlights[start+loc[0]] = r.group + return combineLineMatch(highlights, combineLineMatch(h.highlightEmptyRegion(start, false, lineNum, line[:loc[0]]), h.highlightRegion(start+loc[1], canMatchEnd, lineNum, line[loc[1]:], r))) + } + } + + for _, p := range h.def.rules.patterns { + matches := FindAllIndex(p.regex, line, start == 0, canMatchEnd) + for _, m := range matches { + highlights[start+m[0]] = p.group + highlights[start+m[1]] = "" + } + } + + if canMatchEnd { + h.states[lineNum] = nil + } + + return highlights +} + +func (h *Highlighter) Highlight(input string, startline int) []LineMatch { + lines := strings.Split(input, "\n") + var lineMatches []LineMatch + + h.states = make([]*Region, len(lines)) + + for i := startline; i < len(lines); i++ { + line := []byte(lines[i]) + if i == 0 || h.states[i-1] == nil { + lineMatches = append(lineMatches, h.highlightEmptyRegion(0, true, i, line)) + } else { + lineMatches = append(lineMatches, h.highlightRegion(0, true, i, line, h.states[i-1])) + } + } + + return lineMatches +} diff --git a/parser.go b/parser.go new file mode 100644 index 0000000..4b3e909 --- /dev/null +++ b/parser.go @@ -0,0 +1,154 @@ +package highlight + +import ( + "encoding/json" + "fmt" + "regexp" +) + +// 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 file +type Def struct { + ft string + ftdetect []*regexp.Regexp + rules *Rules +} + +// 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 pattern +type Pattern struct { + group string + regex *regexp.Regexp +} + +// Rules defines which patterns and regions can be used to highlight +// a filetype +type Rules struct { + regions []*Region + patterns []*Pattern +} + +// 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 regions +type Region struct { + group string + parent *Region + start *regexp.Regexp + end *regexp.Regexp + rules *Rules +} + +// ParseDef parses an input syntax file into a highlight Def +// Note that ParseDef may return multiple errors +func ParseDef(input []byte) (*Def, error) { + var rules map[string]interface{} + if err := json.Unmarshal(input, &rules); err != nil { + return nil, err + } + + s := new(Def) + + for k, v := range rules { + if k == "filetype" { + filetype := v.(string) + + s.ft = filetype + } else if k == "detect" { + ftdetect := v.(map[string]interface{}) + if len(ftdetect) >= 1 { + syntax, err := regexp.Compile(ftdetect["filename"].(string)) + if err != nil { + return nil, err + } + + s.ftdetect = append(s.ftdetect, syntax) + } + if len(ftdetect) >= 2 { + header, err := regexp.Compile(ftdetect["header"].(string)) + if err != nil { + return nil, err + } + + s.ftdetect = append(s.ftdetect, header) + } + } else if k == "rules" { + inputRules := v.([]interface{}) + + rules, err := parseRules(inputRules, nil) + if err != nil { + return nil, err + } + + s.rules = rules + } + } + + return s, nil +} + +func parseRules(input []interface{}, curRegion *Region) (*Rules, error) { + rules := new(Rules) + + for _, v := range input { + rule := v.(map[string]interface{}) + for k, val := range rule { + group := k + + switch object := val.(type) { + case string: + // Pattern + r, err := regexp.Compile(object) + if err != nil { + return nil, err + } + + rules.patterns = append(rules.patterns, &Pattern{group, r}) + case map[string]interface{}: + // Region + region, err := parseRegion(group, object, curRegion) + if err != nil { + return nil, err + } + rules.regions = append(rules.regions, region) + default: + return nil, fmt.Errorf("Bad type %T", object) + } + } + } + + return rules, nil +} + +func parseRegion(group string, regionInfo map[string]interface{}, prevRegion *Region) (*Region, error) { + var err error + + region := new(Region) + region.group = group + region.parent = prevRegion + + region.start, err = regexp.Compile(regionInfo["start"].(string)) + + if err != nil { + return nil, err + } + + region.end, err = regexp.Compile(regionInfo["end"].(string)) + + if err != nil { + return nil, err + } + + region.rules, err = parseRules(regionInfo["rules"].([]interface{}), region) + + if err != nil { + return nil, err + } + + return region, nil +}