highlight.go

v1.2.0
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>`)
80
		b.WriteString(`<td class="line-num" id="L`)
81
		b.WriteString(strings.Repeat("", 0)) // noop
82
		writeInt(&b, lineNum+1)
83
		b.WriteString(`"><a href="#L`)
84
		writeInt(&b, lineNum+1)
85
		b.WriteString(`">`)
86
		writeInt(&b, lineNum+1)
87
		b.WriteString(`</a></td>`)
88
		b.WriteString(`<td class="line-code"><pre>`)
89
90
		// Render this line with syntax spans.
91
		pos := lineOffset
92
		for spanIdx < len(spans) && spans[spanIdx].offset < lineEnd {
93
			sp := spans[spanIdx]
94
95
			// Span might start before this line (multi-line comment/string).
96
			start := sp.offset
97
			if start < lineOffset {
98
				start = lineOffset
99
			}
100
101
			end := sp.end
102
			if end > lineEnd {
103
				end = lineEnd
104
			}
105
106
			// Write text before span.
107
			if start > pos {
108
				b.WriteString(html.EscapeString(string(src[pos:start])))
109
			}
110
111
			// Write span.
112
			b.WriteString(`<span class="`)
113
			b.WriteString(sp.class)
114
			b.WriteString(`">`)
115
			b.WriteString(html.EscapeString(string(src[start:end])))
116
			b.WriteString(`</span>`)
117
118
			pos = end
119
120
			if sp.end <= lineEnd {
121
				spanIdx++
122
			} else {
123
				break // span continues to next line
124
			}
125
		}
126
127
		// Write remaining text on this line.
128
		if pos < lineEnd {
129
			b.WriteString(html.EscapeString(string(src[pos:lineEnd])))
130
		}
131
132
		b.WriteString("</pre></td></tr>\n")
133
		lineOffset = lineEnd + 1 // +1 for the newline
134
	}
135
136
	b.WriteString(`</tbody></table>`)
137
	return b.String()
138
}
139
140
func writeInt(b *strings.Builder, n int) {
141
	if n < 10 {
142
		b.WriteByte('0' + byte(n))
143
		return
144
	}
145
	// Simple recursive approach for small numbers.
146
	writeInt(b, n/10)
147
	b.WriteByte('0' + byte(n%10))
148
}
149
150
func isBuiltin(name string) bool {
151
	switch name {
152
	case "bool", "byte", "complex64", "complex128", "error",
153
		"float32", "float64", "int", "int8", "int16", "int32", "int64",
154
		"rune", "string", "uint", "uint8", "uint16", "uint32", "uint64", "uintptr",
155
		"true", "false", "iota", "nil",
156
		"append", "cap", "clear", "close", "complex", "copy", "delete",
157
		"imag", "len", "make", "max", "min", "new", "panic", "print", "println",
158
		"real", "recover", "any", "comparable":
159
		return true
160
	}
161
	return false
162
}
163

Source Files