new: language auto-detect - /api/guess-language, refresh button, guess on paste
This commit is contained in:
@@ -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})
|
||||||
|
}
|
||||||
@@ -327,6 +327,7 @@ func (a *apiServer) routes() http.Handler {
|
|||||||
r.Delete("/pastes/{id}", a.handleDeletePaste)
|
r.Delete("/pastes/{id}", a.handleDeletePaste)
|
||||||
r.Delete("/pastes/{id}/redeem", a.handleRedeemDeletion)
|
r.Delete("/pastes/{id}/redeem", a.handleRedeemDeletion)
|
||||||
r.Get("/public", a.handleListPublic)
|
r.Get("/public", a.handleListPublic)
|
||||||
|
r.Post("/guess-language", a.handleGuessLang)
|
||||||
r.Post("/pastes/can", a.handleCreateCan)
|
r.Post("/pastes/can", a.handleCreateCan)
|
||||||
r.Get("/cans/{id}", a.handleGetCan)
|
r.Get("/cans/{id}", a.handleGetCan)
|
||||||
r.Get("/cans/{id}/items/{item}", a.handleCanItem)
|
r.Get("/cans/{id}/items/{item}", a.handleCanItem)
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
@@ -178,3 +178,5 @@ td a.slug:hover { color: var(--accent); }
|
|||||||
.center .btn { width: 100%; margin-top: 12px; }
|
.center .btn { width: 100%; margin-top: 12px; }
|
||||||
.err { display: none; margin-top: 12px; font-size: 12.5px; color: #ff8fa3; }
|
.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); }
|
.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; }
|
||||||
|
|||||||
@@ -11,6 +11,7 @@
|
|||||||
<option>bash</option><option>sql</option><option>yaml</option><option>json</option>
|
<option>bash</option><option>sql</option><option>yaml</option><option>json</option>
|
||||||
<option>markdown</option><option>text</option>
|
<option>markdown</option><option>text</option>
|
||||||
</select>
|
</select>
|
||||||
|
<button class="btn btn-icon" id="reguess" title="Re-detect language" type="button">⟳</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="editor-wrap">
|
<div class="editor-wrap">
|
||||||
<div class="gutter" id="gutter">1</div>
|
<div class="gutter" id="gutter">1</div>
|
||||||
@@ -67,6 +68,30 @@ updateGutter();
|
|||||||
|
|
||||||
$('haspw').addEventListener('change', e => { $('password').style.display = e.target.checked ? 'block' : 'none'; });
|
$('haspw').addEventListener('change', e => { $('password').style.display = e.target.checked ? 'block' : 'none'; });
|
||||||
|
|
||||||
|
let guessed = ''; // last auto-detected language, '' = user override
|
||||||
|
|
||||||
|
async function guessLang() {
|
||||||
|
if (!content.value.trim()) return;
|
||||||
|
try {
|
||||||
|
const res = await fetch('/api/guess-language', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {'Content-Type': 'application/json'},
|
||||||
|
body: JSON.stringify({content: content.value}),
|
||||||
|
});
|
||||||
|
const data = await res.json();
|
||||||
|
if (res.ok && data.language) {
|
||||||
|
guessed = data.language;
|
||||||
|
$('language').value = data.language;
|
||||||
|
}
|
||||||
|
} catch(e) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
// refresh button: always re-detect, even if user picked something
|
||||||
|
$('reguess').addEventListener('click', guessLang);
|
||||||
|
|
||||||
|
// auto-guess when pasting into the editor
|
||||||
|
content.addEventListener('paste', () => setTimeout(guessLang, 0));
|
||||||
|
|
||||||
async function create() {
|
async function create() {
|
||||||
const body = {
|
const body = {
|
||||||
content: content.value,
|
content: content.value,
|
||||||
|
|||||||
Reference in New Issue
Block a user