| 🛟 Updated. 28d5985 k33g 20h ago | 1 | package syntax |
| 2 | |
| 3 | import ( |
| 4 | "sort" |
| 5 | "strings" |
| 6 | "unicode/utf8" |
| 7 | ) |
| 8 | |
| 9 | // Highlight returns the spans to colour, one slice per line of src. |
| 10 | // |
| 11 | // The result always has exactly as many entries as src has lines, so the |
| 12 | // editor can index it by line number without checking. Text no span covers — |
| 13 | // whitespace, and anything the scanner could make nothing of — is drawn in the |
| 14 | // editor's plain text style. |
| 15 | // |
| 16 | // A language this package does not colour gives one empty slice per line, |
| 17 | // rather than nothing, so the caller still indexes it the same way. |
| 18 | // |
| 19 | // spans := syntax.Highlight(syntax.LanguageMarkdown, "# Title\n") |
| 20 | // fmt.Println(spans[0][0].Class) // heading |
| 21 | func Highlight(language Language, src string) [][]Span { |
| 22 | if definition, ok := registry[language]; ok && definition.Highlight != nil { |
| 23 | return definition.Highlight(src) |
| 24 | } |
| 25 | return make([][]Span, NewLineIndex(src).Count()) |
| 26 | } |
| 27 | |
| 28 | // LineIndex knows where each line of a source text starts, and how many runes |
| 29 | // it holds, which is what turns a byte range into editor coordinates. |
| 30 | // |
| 31 | // It is exported because a scanner that works in byte offsets — anything built |
| 32 | // on a tokeniser rather than on LineScanner, such as Go's go/scanner — needs it |
| 33 | // to turn those offsets into the per-line spans Highlight promises. |
| 34 | // |
| 35 | // lines := syntax.NewLineIndex(src) |
| 36 | // out := make([][]syntax.Span, lines.Count()) |
| 37 | // lines.AppendSpans(out, start, end, syntax.ClassKeyword) |
| 38 | type LineIndex struct { |
| 39 | src string |
| 40 | starts []int // byte offset of the first character of each line |
| 41 | runeLens []int // number of runes on each line |
| 42 | } |
| 43 | |
| 44 | // NewLineIndex builds the index for a source text. |
| 45 | func NewLineIndex(src string) *LineIndex { |
| 46 | index := &LineIndex{src: src, starts: []int{0}} |
| 47 | for offset, r := range src { |
| 48 | if r == '\n' { |
| 49 | index.starts = append(index.starts, offset+1) |
| 50 | } |
| 51 | } |
| 52 | |
| 53 | index.runeLens = make([]int, len(index.starts)) |
| 54 | for line := range index.starts { |
| 55 | index.runeLens[line] = utf8.RuneCountInString(index.text(line)) |
| 56 | } |
| 57 | return index |
| 58 | } |
| 59 | |
| 60 | // Count returns the number of lines, always at least one. |
| 61 | func (x *LineIndex) Count() int { return len(x.starts) } |
| 62 | |
| 63 | // text returns the content of a line, without its terminator. |
| 64 | func (x *LineIndex) text(line int) string { |
| 65 | start := x.starts[line] |
| 66 | if line+1 < len(x.starts) { |
| 67 | return strings.TrimSuffix(x.src[start:x.starts[line+1]-1], "\r") |
| 68 | } |
| 69 | return x.src[start:] |
| 70 | } |
| 71 | |
| 72 | // AppendSpans records the byte range [start, end) as one span per line it |
| 73 | // covers, since a span may never straddle a line break. |
| 74 | // |
| 75 | // out must already have one entry per line — Count says how many — and is |
| 76 | // appended to in place. |
| 77 | func (x *LineIndex) AppendSpans(out [][]Span, start, end int, class Class) { |
| 78 | firstLine, firstCol := x.position(start) |
| 79 | lastLine, lastCol := x.position(end) |
| 80 | |
| 81 | if firstLine == lastLine { |
| 82 | x.add(out, firstLine, firstCol, lastCol, class) |
| 83 | return |
| 84 | } |
| 85 | |
| 86 | x.add(out, firstLine, firstCol, x.runeLens[firstLine], class) |
| 87 | for line := firstLine + 1; line < lastLine; line++ { |
| 88 | x.add(out, line, 0, x.runeLens[line], class) |
| 89 | } |
| 90 | x.add(out, lastLine, 0, lastCol, class) |
| 91 | } |
| 92 | |
| 93 | // add appends one span, dropping the empty ones so that callers never have to |
| 94 | // check for them. |
| 95 | func (x *LineIndex) add(out [][]Span, line, start, end int, class Class) { |
| 96 | if line < 0 || line >= len(out) || end <= start { |
| 97 | return |
| 98 | } |
| 99 | out[line] = append(out[line], Span{Start: start, End: end, Class: class}) |
| 100 | } |
| 101 | |
| 102 | // position converts a byte offset into a line number and a rune column. |
| 103 | func (x *LineIndex) position(offset int) (line, col int) { |
| 104 | if offset <= 0 { |
| 105 | return 0, 0 |
| 106 | } |
| 107 | if offset > len(x.src) { |
| 108 | offset = len(x.src) |
| 109 | } |
| 110 | |
| 111 | // The line is the last one starting at or before the offset. |
| 112 | line = sort.SearchInts(x.starts, offset+1) - 1 |
| 113 | return line, utf8.RuneCountInString(x.src[x.starts[line]:offset]) |
| 114 | } |