1package tray
2
3import (
4 "strconv"
5 "strings"
6 "unicode/utf8"
7)
8
9// maxTipBytes bounds a tooltip text sent to the bar.
10const maxTipBytes = 512
11
12// plainText turns the markup an application puts in a tooltip into plain
13// text: tags are dropped, <br> becomes a newline, and the common entities are
14// resolved. The result is cut to maxTipBytes at a rune boundary. A "<" with no
15// closing ">" is kept as text.
16func plainText(s string) string {
17 var b strings.Builder
18 for i := 0; i < len(s) && b.Len() < maxTipBytes+utf8.UTFMax; {
19 switch c := s[i]; {
20 case c == '<':
21 end := strings.IndexByte(s[i:], '>')
22 if end < 0 {
23 b.WriteByte(c)
24 i++
25 continue
26 }
27 if isBreak(s[i+1 : i+end]) {
28 b.WriteByte('\n')
29 }
30 i += end + 1
31 case c == '&':
32 if r, n, ok := entity(s[i:]); ok {
33 b.WriteRune(r)
34 i += n
35 continue
36 }
37 b.WriteByte(c)
38 i++
39 default:
40 b.WriteByte(c)
41 i++
42 }
43 }
44 return cutRunes(b.String(), maxTipBytes)
45}
46
47// isBreak reports whether the inside of a tag is <br>, <br/> or <br />.
48func isBreak(tag string) bool {
49 tag = strings.TrimSpace(strings.TrimSuffix(strings.TrimSpace(tag), "/"))
50 return strings.EqualFold(tag, "br")
51}
52
53// entity decodes the entity at the start of s ("&", "A", "A").
54func entity(s string) (r rune, n int, ok bool) {
55 end := strings.IndexByte(s, ';')
56 if end < 2 || end > 10 {
57 return 0, 0, false
58 }
59 switch name := s[1:end]; name {
60 case "amp":
61 return '&', end + 1, true
62 case "lt":
63 return '<', end + 1, true
64 case "gt":
65 return '>', end + 1, true
66 case "quot":
67 return '"', end + 1, true
68 case "apos":
69 return '\'', end + 1, true
70 default:
71 if name[0] != '#' {
72 return 0, 0, false
73 }
74 num, base := name[1:], 10
75 if len(num) > 0 && (num[0] == 'x' || num[0] == 'X') {
76 num, base = num[1:], 16
77 }
78 v, err := strconv.ParseUint(num, base, 32)
79 if err != nil || !utf8.ValidRune(rune(v)) || v == 0 {
80 return 0, 0, false
81 }
82 return rune(v), end + 1, true
83 }
84}
85
86// cutRunes cuts s to at most max bytes without splitting a rune.
87func cutRunes(s string, max int) string {
88 if len(s) <= max {
89 return s
90 }
91 for max > 0 && !utf8.RuneStart(s[max]) {
92 max--
93 }
94 return s[:max]
95}