package main
import (
"html"
"regexp"
"strings"
)
// Minimal regex-based syntax highlighter for the paste view (#1).
// Server-side, no external dependencies. Tokens: comments, strings,
// numbers, keywords. Output is HTML with span classes styled in app.css.
// Highlighting is applied per line so the gutter stays line-aligned.
type hlLang struct {
keywords map[string]bool
lineComps []string // line comment prefixes
blockCom [2]string
}
var hlLangs = map[string]hlLang{
"go": {
keywords: set("break case chan const continue default defer else fallthrough for func go goto if import interface map package range return select struct switch type var nil true false string int int64 int32 uint byte rune bool float64 float32 error make new len cap append panic recover"),
lineComps: []string{"//"},
blockCom: [2]string{"/*", "*/"},
},
"python": {
keywords: set("and as assert async await break class continue def del elif else except False finally for from global if import in is lambda None nonlocal not or pass raise return True try while with yield self print len range str int float list dict set tuple open"),
lineComps: []string{"#"},
},
"javascript": {
keywords: set("async await break case catch class const continue debugger default delete do else export extends finally for function if import in instanceof let new null of return static super switch this throw true false try typeof undefined var void while with yield console log document window Math JSON Array Object String Number Boolean Promise"),
lineComps: []string{"//"},
blockCom: [2]string{"/*", "*/"},
},
"json": {
keywords: set("true false null"),
},
"bash": {
keywords: set("if then else elif fi for while do done case esac function return exit local export echo cd ls grep awk sed cat curl sudo apt git make echo read shift set unset trap source alias printf test rm mv cp mkdir chmod chown"),
lineComps: []string{"#"},
},
"sql": {
keywords: set("SELECT FROM WHERE INSERT INTO VALUES UPDATE SET DELETE CREATE TABLE DROP ALTER INDEX JOIN LEFT RIGHT INNER OUTER ON GROUP BY ORDER HAVING LIMIT OFFSET AND OR NOT NULL IS IN AS DISTINCT UNION ALL PRIMARY KEY FOREIGN REFERENCES DEFAULT UNIQUE CHECK VIEW WITH RETURNING EXISTS CASE WHEN THEN ELSE END COUNT SUM AVG MIN MAX"),
lineComps: []string{"--"},
blockCom: [2]string{"/*", "*/"},
},
}
// aliases from the language dropdown / guesser
var hlAliases = map[string]string{
"py": "python", "python3": "python",
"js": "javascript", "node": "javascript", "typescript": "javascript", "ts": "javascript",
"sh": "bash", "shell": "bash", "zsh": "bash",
"golang": "go",
"c": "go", "cpp": "go", "c++": "go", "java": "go", "rust": "go", "rs": "go",
// C-family shares the same token rules as Go for highlighting purposes
}
func set(words string) map[string]bool {
m := make(map[string]bool)
for _, w := range strings.Fields(words) {
m[w] = true
}
return m
}
func resolveLang(lang string) (string, hlLang, bool) {
l := strings.ToLower(strings.TrimSpace(lang))
if l == "" || l == "text" || l == "markdown" || l == "yaml" {
return "", hlLang{}, false
}
if l == "yml" {
return "", hlLang{}, false
}
if g, ok := hlAliases[l]; ok {
if h, ok2 := hlLangs[g]; ok2 {
return g, h, true
}
return "", hlLang{}, false
}
h, ok := hlLangs[l]
return l, h, ok
}
var hlTokenRe = regexp.MustCompile(`("(?:[^"\\]|\\.)*"?|'(?:[^'\\]|\\.)*'?|` + "`" + `[^` + "`" + `]*` + "`" + `?|//[^\n]*|--[^\n]*|#[^\n]*|/\*.*?(?:\*/|$)|\b(?:[0-9]+\.?[0-9]*|0x[0-9a-fA-F]+)\b|[A-Za-z_][A-Za-z0-9_]*)`)
func highlightLine(line string, h hlLang, lang string) string {
var b strings.Builder
rest := line
// strip a trailing block-comment opener handled below; regex covers it
for {
loc := hlTokenRe.FindStringIndex(rest)
if loc == nil {
b.WriteString(html.EscapeString(rest))
break
}
b.WriteString(html.EscapeString(rest[:loc[0]]))
tok := rest[loc[0]:loc[1]]
cls := ""
switch {
case strings.HasPrefix(tok, "//") || strings.HasPrefix(tok, "#") ||
strings.HasPrefix(tok, "--") || strings.HasPrefix(tok, "/*"):
// '#/--' are comments only in langs that use them
if (strings.HasPrefix(tok, "#") && !containsStr(h.lineComps, "#")) ||
(strings.HasPrefix(tok, "--") && !containsStr(h.lineComps, "--")) {
cls = ""
} else {
cls = "tok-com"
}
case strings.HasPrefix(tok, "\"") || strings.HasPrefix(tok, "'") || strings.HasPrefix(tok, "`"):
cls = "tok-str"
case tok[0] >= '0' && tok[0] <= '9':
cls = "tok-num"
case h.keywords[tok]:
cls = "tok-kw"
}
if cls != "" {
b.WriteString(`` + html.EscapeString(tok) + ``)
} else {
b.WriteString(html.EscapeString(tok))
}
rest = rest[loc[1]:]
}
return b.String()
}
func containsStr(list []string, s string) bool {
for _, v := range list {
if v == s {
return true
}
}
return false
}
// highlightCode returns HTML with highlighting spans; safe because all
// non-token text is html-escaped.
func highlightCode(content, lang string) string {
l, h, ok := resolveLang(lang)
_ = l
if !ok {
return html.EscapeString(content)
}
lines := strings.Split(content, "\n")
out := make([]string, len(lines))
for i, line := range lines {
out[i] = highlightLine(line, h, lang)
}
return strings.Join(out, "\n")
}