diff --git a/guess.go b/guess.go new file mode 100644 index 0000000..3f471e8 --- /dev/null +++ b/guess.go @@ -0,0 +1,74 @@ +package main + +import ( + "encoding/json" + "net/http" + "regexp" + "strings" +) + +// guessLang heuristically detects a language from source content. +func guessLang(s string) string { + src := strings.TrimSpace(s) + if src == "" { + return "" + } + + // JSON: must start with { or [ and parse + if src[0] == '{' || src[0] == '[' { + var v any + if json.Unmarshal([]byte(src), &v) == nil { + return "json" + } + } + + rules := []struct { + lang string + re *regexp.Regexp + }{ + {"python", regexp.MustCompile(`(?m)^\s*(def |class |import |from \w+ import |@decorator)`)}, + {"python", regexp.MustCompile(`(^|\n)\s*#!.*python`)}, + {"go", regexp.MustCompile(`(?m)^\s*(package \w+|import \(|func (\w+|\() )`)}, + {"rust", regexp.MustCompile(`(?m)(\bfn \w+|let mut \b|\bimpl \b|\bmatch \w+ \{|use std::)`)}, + {"javascript", regexp.MustCompile(`(?m)(\bconst \w+ = |require\(|import \w+ from |=> \{|\bconsole\.log\()` )}, + {"java", regexp.MustCompile(`(?m)(\bpublic (static |final |class )|\bSystem\.out\.print|import java\.)`)}, + {"c", regexp.MustCompile(`(?m)(#include\s*<\w+\.h>|printf\(|\bint main\()` )}, + {"cpp", regexp.MustCompile(`(?m)(#include\s*<(iostream|vector|string)>|std::|\bcout\s*<<)`)}, + {"bash", regexp.MustCompile(`(?m)^(#!.*bash|#!.*sh|\w+\(\)\s*\{)` )}, + {"sql", regexp.MustCompile(`(?i)\b(SELECT .+ FROM|INSERT INTO|CREATE TABLE|UPDATE \w+ SET)\b`)}, + {"yaml", regexp.MustCompile(`(?m)^(\w[\w-]*:\s*(\||\S)| \w[\w-]*: |---\s*$)`)}, + {"markdown", regexp.MustCompile("(?m)^(#{1,6} \\S|\\|.*\\||-\\s\\[\\s?\\]|```)")}, + } + + scores := map[string]int{} + for _, r := range rules { + n := len(r.re.FindAllString(src, -1)) + if n > 0 { + // later generic rules shouldn't drown out earlier specific ones + scores[r.lang] += n + } + } + + best, bestN := "", 0 + for lang, n := range scores { + if n > bestN { + best, bestN = lang, n + } + } + if bestN == 0 { + return "text" + } + return best +} + +func (a *apiServer) handleGuessLang(w http.ResponseWriter, r *http.Request) { + var req struct { + Content string `json:"content"` + } + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + writeErr(w, http.StatusBadRequest, "invalid json body") + return + } + lang := guessLang(req.Content) + writeJSON(w, http.StatusOK, map[string]any{"language": lang}) +} diff --git a/main.go b/main.go index 923553d..3769b34 100644 --- a/main.go +++ b/main.go @@ -327,6 +327,7 @@ func (a *apiServer) routes() http.Handler { r.Delete("/pastes/{id}", a.handleDeletePaste) r.Delete("/pastes/{id}/redeem", a.handleRedeemDeletion) r.Get("/public", a.handleListPublic) + r.Post("/guess-language", a.handleGuessLang) r.Post("/pastes/can", a.handleCreateCan) r.Get("/cans/{id}", a.handleGetCan) r.Get("/cans/{id}/items/{item}", a.handleCanItem) diff --git a/palette b/palette index 6351c8f..b8bde3a 100755 Binary files a/palette and b/palette differ diff --git a/palette.db-shm b/palette.db-shm index 19d280e..52bf5aa 100644 Binary files a/palette.db-shm and b/palette.db-shm differ diff --git a/palette.db-wal b/palette.db-wal index 8856206..acaeaad 100644 Binary files a/palette.db-wal and b/palette.db-wal differ diff --git a/web/static/app.css b/web/static/app.css index 65393c9..ef87fe0 100644 --- a/web/static/app.css +++ b/web/static/app.css @@ -178,3 +178,5 @@ td a.slug:hover { color: var(--accent); } .center .btn { width: 100%; margin-top: 12px; } .err { display: none; margin-top: 12px; font-size: 12.5px; color: #ff8fa3; } .center .foot { font-size: 12px; color: var(--muted-fg); padding: 14px; border-top: 1px solid var(--border); } + +.btn-icon { padding: 6px 10px; font-size: 14px; line-height: 1; } diff --git a/web/templates/new.html b/web/templates/new.html index 3e1af90..f5537cd 100644 --- a/web/templates/new.html +++ b/web/templates/new.html @@ -11,6 +11,7 @@ +