77 lines
2.7 KiB
Go
77 lines
2.7 KiB
Go
package renderkit
|
|
|
|
import (
|
|
stdhtml "html"
|
|
"path"
|
|
"regexp"
|
|
"strings"
|
|
)
|
|
|
|
// Snippet describes one fenced code block in an assistant reply that is
|
|
// worth offering as a download.
|
|
type Snippet struct {
|
|
Index int // position among ALL fenced blocks in the reply (matches the DOM order of .code-block)
|
|
Lang string // lower-cased fence language
|
|
Ext string // file extension including the dot, e.g. ".go"
|
|
Label string // display name of the type, e.g. "Go"
|
|
Lines int
|
|
Bytes int
|
|
// Filename is set when the block's first line is a comment naming a
|
|
// file with the matching extension (e.g. "// internal/db/users.go"),
|
|
// which models often write; only the base name is kept.
|
|
Filename string
|
|
}
|
|
|
|
var firstLineFilename = regexp.MustCompile(`^\s*(?://|#|--|/\*|<!--)\s*([\w./-]+\.\w+)\s*(?:\*/|-->)?\s*$`)
|
|
|
|
func filenameHint(body, ext string) string {
|
|
first, _, _ := strings.Cut(body, "\n")
|
|
m := firstLineFilename.FindStringSubmatch(first)
|
|
if m == nil {
|
|
return ""
|
|
}
|
|
base := path.Base(m[1])
|
|
if path.Ext(base) != ext || base == ext {
|
|
return ""
|
|
}
|
|
return base
|
|
}
|
|
|
|
type snippetType struct{ ext, label string }
|
|
|
|
var snippetTypes = map[string]snippetType{
|
|
"go": {".go", "Go"}, "golang": {".go", "Go"},
|
|
"js": {".js", "JavaScript"}, "javascript": {".js", "JavaScript"},
|
|
"jsx": {".jsx", "JSX"}, "ts": {".ts", "TypeScript"}, "typescript": {".ts", "TypeScript"}, "tsx": {".tsx", "TSX"},
|
|
"py": {".py", "Python"}, "python": {".py", "Python"},
|
|
"sh": {".sh", "Shell"}, "bash": {".sh", "Shell"}, "shell": {".sh", "Shell"}, "zsh": {".sh", "Shell"},
|
|
"json": {".json", "JSON"}, "sql": {".sql", "SQL"},
|
|
"c": {".c", "C"}, "h": {".h", "C header"}, "cpp": {".cpp", "C++"}, "c++": {".cpp", "C++"},
|
|
"java": {".java", "Java"}, "rust": {".rs", "Rust"}, "rs": {".rs", "Rust"},
|
|
"diff": {".patch", "Patch"}, "patch": {".patch", "Patch"},
|
|
"yaml": {".yml", "YAML"}, "yml": {".yml", "YAML"}, "toml": {".toml", "TOML"},
|
|
"html": {".html", "HTML"}, "css": {".css", "CSS"},
|
|
"md": {".md", "Markdown"}, "markdown": {".md", "Markdown"},
|
|
}
|
|
|
|
// Snippets lists the fenced code blocks in src that have a recognised
|
|
// language, in the order Markdown renders them. Blocks with no language (or
|
|
// an unknown one) are skipped, but still counted in Index.
|
|
func Snippets(src string) []Snippet {
|
|
var out []Snippet
|
|
for i, m := range fencedCodeBlock.FindAllStringSubmatch(rawMarkdown(src), -1) {
|
|
lang := strings.ToLower(m[1])
|
|
t, ok := snippetTypes[lang]
|
|
if !ok {
|
|
continue
|
|
}
|
|
body := stdhtml.UnescapeString(m[2])
|
|
out = append(out, Snippet{
|
|
Index: i, Lang: lang, Ext: t.ext, Label: t.label,
|
|
Lines: strings.Count(strings.TrimRight(body, "\n"), "\n") + 1,
|
|
Bytes: len(body), Filename: filenameHint(body, t.ext),
|
|
})
|
|
}
|
|
return out
|
|
}
|