1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
|
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])
}
|