package tray
import (
"strconv"
"strings"
"unicode/utf8"
)
// maxTipBytes bounds a tooltip text sent to the bar.
const maxTipBytes = 512
// plainText turns the markup an application puts in a tooltip into plain
// text: tags are dropped,
becomes a newline, and the common entities are
// resolved. The result is cut to maxTipBytes at a rune boundary. A "<" with no
// closing ">" is kept as text.
func plainText(s string) string {
var b strings.Builder
for i := 0; i < len(s) && b.Len() < maxTipBytes+utf8.UTFMax; {
switch c := s[i]; {
case c == '<':
end := strings.IndexByte(s[i:], '>')
if end < 0 {
b.WriteByte(c)
i++
continue
}
if isBreak(s[i+1 : i+end]) {
b.WriteByte('\n')
}
i += end + 1
case c == '&':
if r, n, ok := entity(s[i:]); ok {
b.WriteRune(r)
i += n
continue
}
b.WriteByte(c)
i++
default:
b.WriteByte(c)
i++
}
}
return cutRunes(b.String(), maxTipBytes)
}
// isBreak reports whether the inside of a tag is
,
or
.
func isBreak(tag string) bool {
tag = strings.TrimSpace(strings.TrimSuffix(strings.TrimSpace(tag), "/"))
return strings.EqualFold(tag, "br")
}
// entity decodes the entity at the start of s ("&", "A", "A").
func entity(s string) (r rune, n int, ok bool) {
end := strings.IndexByte(s, ';')
if end < 2 || end > 10 {
return 0, 0, false
}
switch name := s[1:end]; name {
case "amp":
return '&', end + 1, true
case "lt":
return '<', end + 1, true
case "gt":
return '>', end + 1, true
case "quot":
return '"', end + 1, true
case "apos":
return '\'', end + 1, true
default:
if name[0] != '#' {
return 0, 0, false
}
num, base := name[1:], 10
if len(num) > 0 && (num[0] == 'x' || num[0] == 'X') {
num, base = num[1:], 16
}
v, err := strconv.ParseUint(num, base, 32)
if err != nil || !utf8.ValidRune(rune(v)) || v == 0 {
return 0, 0, false
}
return rune(v), end + 1, true
}
}
// cutRunes cuts s to at most max bytes without splitting a rune.
func cutRunes(s string, max int) string {
if len(s) <= max {
return s
}
for max > 0 && !utf8.RuneStart(s[max]) {
max--
}
return s[:max]
}