#1: server-side regex highlighter (highlight.go) for go/python/js/json/bash/sql; token span classes styled in app.css; per-line so gutter stays aligned. #2: in-memory token-bucket rate limiter (ratelimit.go) on POST /api/pastes, /api/guess-language and unlock POST; 429 + Retry-After + X-RateLimit headers. #26: new-page JS POSTs the password to /{id} with ?next= after creation; the unlock handler honors same-origin ?next= redirect so the creator lands on the unlocked paste. POST /{id} route added. Tests: ratelimit_test.go (burst/429, refill, unlock limit, highlight, auto- unlock e2e); existing tests updated for per-test limiter isolation.
151 lines
5.2 KiB
Go
151 lines
5.2 KiB
Go
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(`<span class="` + cls + `">` + html.EscapeString(tok) + `</span>`)
|
|
} 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")
|
|
}
|