// Package lexer turns source code into syntax-highlighted HTML. // // It is a LEXER, not a parser: it classifies tokens and gives up gracefully on anything it // does not understand. A highlighter that can fail to render is worse than one that // occasionally paints an identifier the wrong colour — the reader can see past a wrong // colour, and cannot see past a blank page. Unterminated strings and comments run to a // sensible boundary rather than throwing, and a language nobody has written a lexer for // comes back escaped and unpainted rather than not coming back. // // Output is HTML, and every run of source text passes through html.EscapeString on the way // out. That is not a nicety: the input is source code, which is full of `<`, `>` and `&` — // a C file is nothing but shifts and arrows, and one of the snippets kjøl's own docs // display is literally a block of HTML. Unescaped, a snippet containing `
: it contains no block elements and
// preserves the source's whitespace exactly, so the does the layout.
func Highlight(lang, src string) string {
switch strings.ToLower(lang) {
case "go":
return HighlightGo(src)
case "c":
return HighlightC(src)
default:
return html.EscapeString(src)
}
}
// ---- the pieces every language in here shares ----------------------------
// 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 Go's 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, StringClass, src[i:j])
return j
}
// callAhead reports whether the next non-space byte at or after i is an opening paren —
// which is the whole of the "is this identifier a function" test. It is a guess, and a good
// one: an identifier immediately before a `(` is being called, declared, or converted to,
// and all three are worth seeing at a glance.
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(``)
}
func isDigit(c byte) bool { return c >= '0' && c <= '9' }
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) }