202 lines
6.5 KiB
Go
202 lines
6.5 KiB
Go
package badge
|
|
|
|
import (
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"fmt"
|
|
"html"
|
|
"strings"
|
|
"unicode/utf8"
|
|
)
|
|
|
|
type Options struct {
|
|
Label string
|
|
Message string
|
|
Extent string
|
|
Preset string
|
|
Article bool
|
|
Style string
|
|
Theme string
|
|
Link string
|
|
LeftColor string
|
|
RightColor string
|
|
}
|
|
|
|
var extentColors = map[string]string{
|
|
"none": "#2f855a", "assisted": "#2b6cb0", "partial": "#b7791f", "mostly": "#c05621", "full": "#c53030",
|
|
}
|
|
|
|
var emojiPresetColors = map[string]string{
|
|
"no-ai": "#2f855a", "research": "#2b6cb0", "summary": "#b7791f", "full": "#c53030",
|
|
}
|
|
|
|
func Render(o Options) ([]byte, string) {
|
|
label := truncate(strings.TrimSpace(o.Label), 40)
|
|
if label == "" {
|
|
label = "AI use"
|
|
}
|
|
message := truncate(strings.TrimSpace(o.Message), 80)
|
|
if message == "" {
|
|
message = o.Extent
|
|
}
|
|
if o.Theme == "emoji" {
|
|
return renderEmoji(o, label, message)
|
|
}
|
|
|
|
style := o.Style
|
|
if style != "flat-square" {
|
|
style = "flat"
|
|
}
|
|
|
|
leftWidth := textWidth(label)
|
|
rightWidth := textWidth(message)
|
|
total := leftWidth + rightWidth
|
|
radius := 3
|
|
if style == "flat-square" {
|
|
radius = 0
|
|
}
|
|
|
|
leftColor, rightColor := "#555555", extentColors[o.Extent]
|
|
if rightColor == "" {
|
|
rightColor = "#4a5568"
|
|
}
|
|
if o.Theme == "mono" {
|
|
leftColor, rightColor = "#262626", "#666666"
|
|
}
|
|
if isHexColor(o.LeftColor) {
|
|
leftColor = o.LeftColor
|
|
}
|
|
if isHexColor(o.RightColor) {
|
|
rightColor = o.RightColor
|
|
}
|
|
|
|
title := html.EscapeString(label + ": " + message)
|
|
labelEsc := html.EscapeString(label)
|
|
messageEsc := html.EscapeString(message)
|
|
openLink, closeLink := svgLink(o.Link)
|
|
|
|
svg := fmt.Sprintf(`<svg xmlns="http://www.w3.org/2000/svg" role="img" aria-label="%s" width="%d" height="20" viewBox="0 0 %d 20"><title>%s</title>%s<linearGradient id="s" x2="0" y2="100%%"><stop offset="0" stop-color="#fff" stop-opacity=".12"/><stop offset="1" stop-opacity=".12"/></linearGradient><clipPath id="r"><rect width="%d" height="20" rx="%d" fill="#fff"/></clipPath><g clip-path="url(#r)"><rect width="%d" height="20" fill="%s"/><rect x="%d" width="%d" height="20" fill="%s"/><rect width="%d" height="20" fill="url(#s)"/></g><g fill="#fff" text-anchor="middle" font-family="Verdana,Geneva,DejaVu Sans,sans-serif" font-size="11"><text x="%d" y="15" fill="#010101" fill-opacity=".3">%s</text><text x="%d" y="14">%s</text><text x="%d" y="15" fill="#010101" fill-opacity=".3">%s</text><text x="%d" y="14">%s</text></g>%s</svg>`,
|
|
title, total, total, title, openLink, total, radius, leftWidth, leftColor, leftWidth, rightWidth, rightColor, total,
|
|
leftWidth/2, labelEsc, leftWidth/2, labelEsc, leftWidth+rightWidth/2, messageEsc, leftWidth+rightWidth/2, messageEsc, closeLink)
|
|
return withETag([]byte(svg))
|
|
}
|
|
|
|
func renderEmoji(o Options, label, message string) ([]byte, string) {
|
|
iconName := emojiIconName(o)
|
|
background := emojiPresetColors[o.Preset]
|
|
if o.Article {
|
|
background = "#7c3aed"
|
|
}
|
|
if background == "" {
|
|
background = extentColors[o.Extent]
|
|
}
|
|
if background == "" {
|
|
background = "#4a5568"
|
|
}
|
|
foreground := "#ffffff"
|
|
if isHexColor(o.LeftColor) {
|
|
foreground = o.LeftColor
|
|
}
|
|
if isHexColor(o.RightColor) {
|
|
background = o.RightColor
|
|
}
|
|
|
|
title := html.EscapeString(label + ": " + message)
|
|
openLink, closeLink := svgLink(o.Link)
|
|
icon := emojiIcon(iconName, foreground)
|
|
svg := fmt.Sprintf(`<svg xmlns="http://www.w3.org/2000/svg" role="img" aria-label="%s" focusable="false" width="64" height="64" viewBox="0 0 64 64" data-theme="emoji" data-icon="%s"><title>%s</title>%s<rect width="64" height="64" rx="14" fill="%s"/><path d="M3 18C14 5 35 1 57 5c3 .5 5 3 5 6v7C40 13 20 14 3 27Z" fill="#fff" opacity=".11"/><rect x=".75" y=".75" width="62.5" height="62.5" rx="13.25" fill="none" stroke="#000" stroke-opacity=".14" stroke-width="1.5"/>%s%s</svg>`,
|
|
title, iconName, title, openLink, background, icon, closeLink)
|
|
return withETag([]byte(svg))
|
|
}
|
|
|
|
func emojiIconName(o Options) string {
|
|
if o.Article {
|
|
return "article"
|
|
}
|
|
switch o.Preset {
|
|
case "no-ai":
|
|
return "human"
|
|
case "research":
|
|
return "research"
|
|
case "summary":
|
|
return "summary"
|
|
case "full":
|
|
return "bolt"
|
|
}
|
|
switch o.Extent {
|
|
case "none":
|
|
return "human"
|
|
case "partial":
|
|
return "partial"
|
|
case "mostly":
|
|
return "mostly"
|
|
case "full":
|
|
return "bolt"
|
|
default:
|
|
return "assisted"
|
|
}
|
|
}
|
|
|
|
func emojiIcon(name, color string) string {
|
|
stroke := ` fill="none" stroke="` + color + `" stroke-width="4.5" stroke-linecap="round" stroke-linejoin="round"`
|
|
switch name {
|
|
case "human":
|
|
return `<circle cx="32" cy="21" r="8"` + stroke + `/><path d="M17 50c1.8-10 7.5-15 15-15s13.2 5 15 15"` + stroke + `/>`
|
|
case "research":
|
|
return `<circle cx="27" cy="27" r="12"` + stroke + `/><path d="m36 36 13 13"` + stroke + `/><path d="M27 21v12M21 27h12"` + stroke + `/>`
|
|
case "summary":
|
|
return `<rect x="17" y="12" width="30" height="40" rx="5"` + stroke + `/><path d="M24 24h16M24 32h16M24 40h11"` + stroke + `/>`
|
|
case "bolt":
|
|
return `<path d="M36 8 18 35h12l-2 21 18-29H34Z" fill="` + color + `"/>`
|
|
case "article":
|
|
return `<rect x="14" y="14" width="15" height="15" rx="4"` + stroke + `/><rect x="35" y="14" width="15" height="15" rx="4"` + stroke + `/><rect x="14" y="35" width="15" height="15" rx="4"` + stroke + `/><rect x="35" y="35" width="15" height="15" rx="4"` + stroke + `/>`
|
|
case "partial":
|
|
return `<circle cx="32" cy="32" r="18"` + stroke + `/><path d="M32 14a18 18 0 0 0 0 36Z" fill="` + color + `"/>`
|
|
case "mostly":
|
|
return `<path d="M18 45V35M32 45V27M46 45V18"` + stroke + `/><path d="M14 50h36"` + stroke + `/>`
|
|
default:
|
|
return `<path d="M32 12c1.5 8.2 5.8 12.5 14 14-8.2 1.5-12.5 5.8-14 14-1.5-8.2-5.8-12.5-14-14 8.2-1.5 12.5-5.8 14-14Z"` + stroke + `/><path d="M46 39c.7 3.8 2.7 5.8 6.5 6.5C48.7 46.2 46.7 48.2 46 52c-.7-3.8-2.7-5.8-6.5-6.5 3.8-.7 5.8-2.7 6.5-6.5Z" fill="` + color + `"/>`
|
|
}
|
|
}
|
|
|
|
func svgLink(link string) (string, string) {
|
|
if strings.HasPrefix(link, "https://") || strings.HasPrefix(link, "http://") {
|
|
return `<a href="` + html.EscapeString(link) + `" target="_top">`, `</a>`
|
|
}
|
|
return "", ""
|
|
}
|
|
|
|
func withETag(data []byte) ([]byte, string) {
|
|
hash := sha256.Sum256(data)
|
|
return data, `"` + hex.EncodeToString(hash[:12]) + `"`
|
|
}
|
|
|
|
func textWidth(s string) int {
|
|
runes := utf8.RuneCountInString(s)
|
|
width := runes*7 + 12
|
|
if width < 40 {
|
|
width = 40
|
|
}
|
|
return width
|
|
}
|
|
|
|
func truncate(s string, max int) string {
|
|
r := []rune(s)
|
|
if len(r) <= max {
|
|
return s
|
|
}
|
|
return string(r[:max-1]) + "…"
|
|
}
|
|
|
|
func isHexColor(s string) bool {
|
|
if len(s) != 7 || s[0] != '#' {
|
|
return false
|
|
}
|
|
for _, r := range s[1:] {
|
|
if !((r >= '0' && r <= '9') || (r >= 'a' && r <= 'f') || (r >= 'A' && r <= 'F')) {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|