package syntax import ( "sort" "strings" "unicode/utf8" ) // Highlight returns the spans to colour, one slice per line of src. // // The result always has exactly as many entries as src has lines, so the // editor can index it by line number without checking. Text no span covers — // whitespace, and anything the scanner could make nothing of — is drawn in the // editor's plain text style. // // A language this package does not colour gives one empty slice per line, // rather than nothing, so the caller still indexes it the same way. // // spans := syntax.Highlight(syntax.LanguageMarkdown, "# Title\n") // fmt.Println(spans[0][0].Class) // heading func Highlight(language Language, src string) [][]Span { if definition, ok := registry[language]; ok && definition.Highlight != nil { return definition.Highlight(src) } return make([][]Span, NewLineIndex(src).Count()) } // LineIndex knows where each line of a source text starts, and how many runes // it holds, which is what turns a byte range into editor coordinates. // // It is exported because a scanner that works in byte offsets — anything built // on a tokeniser rather than on LineScanner, such as Go's go/scanner — needs it // to turn those offsets into the per-line spans Highlight promises. // // lines := syntax.NewLineIndex(src) // out := make([][]syntax.Span, lines.Count()) // lines.AppendSpans(out, start, end, syntax.ClassKeyword) type LineIndex struct { src string starts []int // byte offset of the first character of each line runeLens []int // number of runes on each line } // NewLineIndex builds the index for a source text. func NewLineIndex(src string) *LineIndex { index := &LineIndex{src: src, starts: []int{0}} for offset, r := range src { if r == '\n' { index.starts = append(index.starts, offset+1) } } index.runeLens = make([]int, len(index.starts)) for line := range index.starts { index.runeLens[line] = utf8.RuneCountInString(index.text(line)) } return index } // Count returns the number of lines, always at least one. func (x *LineIndex) Count() int { return len(x.starts) } // text returns the content of a line, without its terminator. func (x *LineIndex) text(line int) string { start := x.starts[line] if line+1 < len(x.starts) { return strings.TrimSuffix(x.src[start:x.starts[line+1]-1], "\r") } return x.src[start:] } // AppendSpans records the byte range [start, end) as one span per line it // covers, since a span may never straddle a line break. // // out must already have one entry per line — Count says how many — and is // appended to in place. func (x *LineIndex) AppendSpans(out [][]Span, start, end int, class Class) { firstLine, firstCol := x.position(start) lastLine, lastCol := x.position(end) if firstLine == lastLine { x.add(out, firstLine, firstCol, lastCol, class) return } x.add(out, firstLine, firstCol, x.runeLens[firstLine], class) for line := firstLine + 1; line < lastLine; line++ { x.add(out, line, 0, x.runeLens[line], class) } x.add(out, lastLine, 0, lastCol, class) } // add appends one span, dropping the empty ones so that callers never have to // check for them. func (x *LineIndex) add(out [][]Span, line, start, end int, class Class) { if line < 0 || line >= len(out) || end <= start { return } out[line] = append(out[line], Span{Start: start, End: end, Class: class}) } // position converts a byte offset into a line number and a rune column. func (x *LineIndex) position(offset int) (line, col int) { if offset <= 0 { return 0, 0 } if offset > len(x.src) { offset = len(x.src) } // The line is the last one starting at or before the offset. line = sort.SearchInts(x.starts, offset+1) - 1 return line, utf8.RuneCountInString(x.src[x.starts[line]:offset]) }