highlight_decl.go

v1.4.0
Doc Versions Source
1
package godoc
2
3
import (
4
	"go/scanner"
5
	"go/token"
6
	"html"
7
	"strings"
8
)
9
10
// HighlightDecl returns syntax-highlighted HTML for a Go declaration snippet.
11
func HighlightDecl(src string) string {
12
	return highlightDecl(src, nil, nil)
13
}
14
15
// HighlightDeclLinked returns syntax-highlighted HTML with cross-references.
16
// symbols maps local names to anchors (e.g., "Client" → "#Client").
17
// importLinks maps package aliases to base doc URLs (e.g., "http" → "https://pkg.go.dev/net/http").
18
// When a qualified name like http.Client is found, it links to <baseURL>#Client.
19
func HighlightDeclLinked(src string, symbols map[string]string, importLinks map[string]string) string {
20
	return highlightDecl(src, symbols, importLinks)
21
}
22
23
type rawToken struct {
24
	offset int
25
	end    int
26
	tok    token.Token
27
	lit    string
28
}
29
30
type declSpan struct {
31
	offset int
32
	end    int
33
	class  string // CSS class (empty if link)
34
	href   string // link target (empty if just a span)
35
}
36
37
func highlightDecl(src string, symbols map[string]string, imports map[string]string) string {
38
	prefix := "package p\n"
39
	full := []byte(prefix + src)
40
41
	fset := token.NewFileSet()
42
	file := fset.AddFile("", fset.Base(), len(full))
43
44
	var sc scanner.Scanner
45
	sc.Init(file, full, nil, scanner.ScanComments)
46
47
	// Collect all tokens.
48
	var tokens []rawToken
49
	for {
50
		pos, tok, lit := sc.Scan()
51
		if tok == token.EOF {
52
			break
53
		}
54
		offset := int(pos) - file.Base() - len(prefix)
55
		length := len(lit)
56
		if tok.IsKeyword() || tok.IsOperator() {
57
			length = len(tok.String())
58
		}
59
		if tok == token.IDENT {
60
			length = len(lit)
61
		}
62
		tokens = append(tokens, rawToken{
63
			offset: offset,
64
			end:    offset + length,
65
			tok:    tok,
66
			lit:    lit,
67
		})
68
	}
69
70
	// Build spans from tokens.
71
	var spans []declSpan
72
	for i := 0; i < len(tokens); i++ {
73
		t := tokens[i]
74
		if t.offset < 0 {
75
			continue
76
		}
77
78
		// Check for pkg.Symbol pattern (IDENT PERIOD IDENT).
79
		if t.tok == token.IDENT && imports != nil {
80
			if baseURL, ok := imports[t.lit]; ok {
81
				if i+2 < len(tokens) && tokens[i+1].tok == token.PERIOD && tokens[i+2].tok == token.IDENT {
82
					sym := tokens[i+2]
83
					href := baseURL + "#" + sym.lit
84
					// Span covers from pkg start to Symbol end.
85
					spans = append(spans, declSpan{
86
						offset: t.offset,
87
						end:    sym.end,
88
						href:   href,
89
					})
90
					i += 2 // skip PERIOD and Symbol
91
					continue
92
				}
93
			}
94
		}
95
96
		var class string
97
		var href string
98
99
		switch {
100
		case t.tok == token.COMMENT:
101
			class = "com"
102
		case t.tok == token.STRING || t.tok == token.CHAR:
103
			class = "str"
104
		case t.tok == token.INT || t.tok == token.FLOAT || t.tok == token.IMAG:
105
			class = "num"
106
		case t.tok.IsKeyword():
107
			class = "kw"
108
		case t.tok == token.IDENT:
109
			if isBuiltin(t.lit) {
110
				class = "bi"
111
			} else if symbols != nil {
112
				if target, ok := symbols[t.lit]; ok {
113
					// Only link if not preceded by "." (qualified name).
114
					if t.offset == 0 || src[t.offset-1] != '.' {
115
						href = target
116
					}
117
				}
118
			}
119
		}
120
121
		if class != "" || href != "" {
122
			spans = append(spans, declSpan{
123
				offset: t.offset,
124
				end:    t.end,
125
				class:  class,
126
				href:   href,
127
			})
128
		}
129
	}
130
131
	// Render.
132
	var b strings.Builder
133
	pos := 0
134
	for _, sp := range spans {
135
		if sp.offset < 0 || sp.offset > len(src) {
136
			continue
137
		}
138
		start := sp.offset
139
		end := sp.end
140
		if end > len(src) {
141
			end = len(src)
142
		}
143
		if start > pos {
144
			b.WriteString(html.EscapeString(src[pos:start]))
145
		}
146
147
		text := html.EscapeString(src[start:end])
148
149
		if sp.href != "" {
150
			b.WriteString(`<a href="`)
151
			b.WriteString(html.EscapeString(sp.href))
152
			b.WriteString(`">`)
153
			b.WriteString(text)
154
			b.WriteString(`</a>`)
155
		} else {
156
			b.WriteString(`<span class="`)
157
			b.WriteString(sp.class)
158
			b.WriteString(`">`)
159
			b.WriteString(text)
160
			b.WriteString(`</span>`)
161
		}
162
163
		pos = end
164
	}
165
	if pos < len(src) {
166
		b.WriteString(html.EscapeString(src[pos:]))
167
	}
168
169
	return b.String()
170
}
171

Source Files