| 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 | |