package webui import ( "html" "strings" ) // Go syntax highlighting, for the code samples a documentation page shows. // // It is a LEXER, not a parser: it classifies tokens and gives up gracefully on anything // it does not understand, because a highlighter that can fail to render is worse than // one that occasionally paints an identifier the wrong colour. Unterminated strings and // comments run to the end of the input rather than throwing. // // Output is HTML, and every run of source text passes through html.EscapeString on the // way out — the input is Go source, which is full of `<`, `>` and `&`, and one of the // snippets this is meant to display is literally a block of HTML. // A code block is dark in BOTH themes — a light code block on a light page is a // different kind of thing, and switching it with the theme means the snippet you were // reading changes colour under you. So these are fixed on-dark colours, not theme // tokens: the surface they sit on never changes. const ( goCommentClass = "text-neutral-400" goStringClass = "text-emerald-300" goKeywordClass = "text-sky-300" goNumberClass = "text-amber-300" goFuncClass = "text-violet-300" ) var goKeywords = map[string]bool{ "break": true, "case": true, "chan": true, "const": true, "continue": true, "default": true, "defer": true, "else": true, "fallthrough": true, "for": true, "func": true, "go": true, "goto": true, "if": true, "import": true, "interface": true, "map": true, "package": true, "range": true, "return": true, "select": true, "struct": true, "switch": true, "type": true, "var": true, // Not keywords to the Go spec — predeclared identifiers — but every editor colours // them, and a reader looking for `nil` is looking for the same kind of thing. "nil": true, "true": true, "false": true, "iota": true, "string": true, "int": true, "int64": true, "float64": true, "bool": true, "byte": true, "rune": true, "any": true, "error": true, } // HighlightGo turns Go source into HTML with the tokens wrapped in coloured spans. // // The result is meant for vdom.Raw inside a
: it contains no block elements and
// preserves the source's whitespace exactly, so the does the layout.
func HighlightGo(src string) string {
var b strings.Builder
b.Grow(len(src) * 2)
i := 0
for i < len(src) {
c := src[i]
switch {
// Line comment — including the //gowasm: directives, which are the most
// important line in several of these snippets.
case c == '/' && i+1 < len(src) && src[i+1] == '/':
end := strings.IndexByte(src[i:], '\n')
if end < 0 {
end = len(src)
} else {
end += i
}
span(&b, goCommentClass, src[i:end])
i = end
// Block comment.
case c == '/' && i+1 < len(src) && src[i+1] == '*':
end := strings.Index(src[i+2:], "*/")
if end < 0 {
end = len(src)
} else {
end = i + 2 + end + 2
}
span(&b, goCommentClass, src[i:end])
i = end
// Interpreted string. Ends at the closing quote or the line's end — an
// unterminated string is a typo in a snippet, not a reason to paint the rest of
// the file green.
case c == '"':
i = quoted(&b, src, i, '"', true)
// Raw string: no escapes, and it may span lines.
case c == '`':
i = quoted(&b, src, i, '`', false)
// Rune literal.
case c == '\'':
i = quoted(&b, src, i, '\'', true)
case isDigit(c):
j := i
for j < len(src) && (isDigit(src[j]) || isHexish(src[j])) {
j++
}
span(&b, goNumberClass, src[i:j])
i = j
case isIdentStart(c):
j := i
for j < len(src) && isIdentPart(src[j]) {
j++
}
word := src[i:j]
switch {
case goKeywords[word]:
span(&b, goKeywordClass, word)
case callAhead(src, j):
// An identifier immediately followed by "(" is being called (or is a type
// being converted to). Colouring it is what makes the shape of a snippet
// readable at a glance.
span(&b, goFuncClass, word)
default:
b.WriteString(html.EscapeString(word))
}
i = j
default:
b.WriteString(html.EscapeString(string(c)))
i++
}
}
return b.String()
}
// quoted consumes a quoted literal starting at i and writes it as a string span.
// escapes reports whether a backslash escapes the next byte (false for raw strings).
func quoted(b *strings.Builder, src string, i int, quote byte, escapes bool) int {
j := i + 1
for j < len(src) {
if escapes && src[j] == '\\' && j+1 < len(src) {
j += 2
continue
}
if src[j] == quote {
j++
break
}
if escapes && src[j] == '\n' {
break // unterminated: stop at the line end rather than eating the file
}
j++
}
span(b, goStringClass, src[i:j])
return j
}
// callAhead reports whether the next non-space byte at or after i is an opening paren.
func callAhead(src string, i int) bool {
for i < len(src) && (src[i] == ' ' || src[i] == '\t') {
i++
}
return i < len(src) && src[i] == '('
}
func span(b *strings.Builder, class, text string) {
b.WriteString(``)
b.WriteString(html.EscapeString(text))
b.WriteString(``)
}
// isDigit already exists in the package (autotable.go) — reused rather than shadowed.
func isHexish(c byte) bool {
return c == '.' || c == 'x' || c == 'X' || c == '_' ||
(c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F')
}
func isIdentStart(c byte) bool { return c == '_' || (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') }
func isIdentPart(c byte) bool { return isIdentStart(c) || isDigit(c) }