highlight.go

v1.4.2
Doc Versions Source
1
package godoc
2
3
import (
4
	"go/scanner"
5
	"go/token"
6
	"html"
7
	"strings"
8
)
9
10
// HighlightGo returns syntax-highlighted HTML for Go source code.
11
// Uses go/scanner to tokenize and wraps tokens in <span> elements with CSS classes.
12
// The output includes line numbers with anchor targets for deep linking.
13
func HighlightGo(src []byte) string {
14
	var b strings.Builder
15
16
	fset := token.NewFileSet()
17
	file := fset.AddFile("", fset.Base(), len(src))
18
19
	var s scanner.Scanner
20
	s.Init(file, src, nil, scanner.ScanComments)
21
22
	type tokenSpan struct {
23
		offset int
24
		end    int
25
		class  string
26
	}
27
28
	var spans []tokenSpan
29
30
	for {
31
		pos, tok, lit := s.Scan()
32
		if tok == token.EOF {
33
			break
34
		}
35
36
		offset := int(pos) - file.Base()
37
		var class string
38
		var length int
39
40
		switch {
41
		case tok == token.COMMENT:
42
			class = "com"
43
			length = len(lit)
44
		case tok == token.STRING || tok == token.CHAR:
45
			class = "str"
46
			length = len(lit)
47
		case tok == token.INT || tok == token.FLOAT || tok == token.IMAG:
48
			class = "num"
49
			length = len(lit)
50
		case tok.IsKeyword():
51
			class = "kw"
52
			length = len(tok.String())
53
		case tok == token.IDENT:
54
			if isBuiltin(lit) {
55
				class = "bi"
56
				length = len(lit)
57
			}
58
		}
59
60
		if class != "" {
61
			spans = append(spans, tokenSpan{
62
				offset: offset,
63
				end:    offset + length,
64
				class:  class,
65
			})
66
		}
67
	}
68
69
	// Build output with line numbers.
70
	lines := strings.Split(string(src), "\n")
71
	b.WriteString(`<table class="source-code"><tbody>`)
72
73
	lineOffset := 0
74
	spanIdx := 0
75
76
	for lineNum, line := range lines {
77
		lineEnd := lineOffset + len(line)
78
79
		b.WriteString(`<tr id="L`)
80
		writeInt(&b, lineNum+1)
81
		b.WriteString(`"><td class="line-num"><a href="#L`)
82
		writeInt(&b, lineNum+1)
83
		b.WriteString(`">`)
84
		writeInt(&b, lineNum+1)
85
		b.WriteString(`</a></td><td class="line-code"><pre>`)
86
87
		// Render this line with syntax spans.
88
		pos := lineOffset
89
		for spanIdx < len(spans) && spans[spanIdx].offset < lineEnd {
90
			sp := spans[spanIdx]
91
92
			// Span might start before this line (multi-line comment/string).
93
			start := sp.offset
94
			if start < lineOffset {
95
				start = lineOffset
96
			}
97
98
			end := sp.end
99
			if end > lineEnd {
100
				end = lineEnd
101
			}
102
103
			// Write text before span.
104
			if start > pos {
105
				b.WriteString(html.EscapeString(string(src[pos:start])))
106
			}
107
108
			// Write span.
109
			b.WriteString(`<span class="`)
110
			b.WriteString(sp.class)
111
			b.WriteString(`">`)
112
			b.WriteString(html.EscapeString(string(src[start:end])))
113
			b.WriteString(`</span>`)
114
115
			pos = end
116
117
			if sp.end <= lineEnd {
118
				spanIdx++
119
			} else {
120
				break // span continues to next line
121
			}
122
		}
123
124
		// Write remaining text on this line.
125
		if pos < lineEnd {
126
			b.WriteString(html.EscapeString(string(src[pos:lineEnd])))
127
		}
128
129
		b.WriteString("</pre></td></tr>\n")
130
		lineOffset = lineEnd + 1 // +1 for the newline
131
	}
132
133
	b.WriteString(`</tbody></table>`)
134
	return b.String()
135
}
136
137
func writeInt(b *strings.Builder, n int) {
138
	if n < 10 {
139
		b.WriteByte('0' + byte(n))
140
		return
141
	}
142
	// Simple recursive approach for small numbers.
143
	writeInt(b, n/10)
144
	b.WriteByte('0' + byte(n%10))
145
}
146
147
func isBuiltin(name string) bool {
148
	switch name {
149
	case "bool", "byte", "complex64", "complex128", "error",
150
		"float32", "float64", "int", "int8", "int16", "int32", "int64",
151
		"rune", "string", "uint", "uint8", "uint16", "uint32", "uint64", "uintptr",
152
		"true", "false", "iota", "nil",
153
		"append", "cap", "clear", "close", "complex", "copy", "delete",
154
		"imag", "len", "make", "max", "min", "new", "panic", "print", "println",
155
		"real", "recover", "any", "comparable":
156
		return true
157
	}
158
	return false
159
}
160

Source Files