turbo-editors/turbo-corepublic Fork 0
v0.9.0
Commits
Clone
git clone https://git.rickub.com/turbo-editors/turbo-core.git
git clone ssh://git@rickub.com/turbo-editors/turbo-core.git

Host key fingerprint (ed25519): SHA256:iycHnxEyq0Q7uyVpB7JlznP0G7JrTPXLYRcAU5CSLhc — verify it before your first connect.

🛟 Updated. 28d5985 · on v0.9.0 · k33g · 18h ago
highlight.go · 114 lines · 3.7 KBGo Blame HistoryRaw
  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])
}