-
Notifications
You must be signed in to change notification settings - Fork 44
/
Copy pathoutput.go
188 lines (162 loc) · 4.75 KB
/
output.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
package terminal
import (
"html"
"html/template"
"slices"
"strconv"
"strings"
"time"
)
var (
timeTagImpl = template.Must(template.New("time").Parse(
`<time datetime="{{.}}">{{.}}</time>`,
))
openSpanTagTmpl = template.Must(template.New("span").Parse(
`<span class="{{.}}">`,
))
)
type outputBuffer struct {
buf strings.Builder
}
func (b *outputBuffer) appendNodeStyle(n node) {
openSpanTagTmpl.Execute(&b.buf, strings.Join(n.style.asClasses(), " "))
}
func (b *outputBuffer) closeStyle() {
b.buf.WriteString("</span>")
}
func (b *outputBuffer) appendAnchor(url string) {
b.buf.WriteString(`<a href="`)
b.buf.WriteString(html.EscapeString(sanitizeURL(url)))
b.buf.WriteString(`">`)
}
func (b *outputBuffer) closeAnchor() {
b.buf.WriteString("</a>")
}
func (b *outputBuffer) appendMeta(namespace string, data map[string]string) {
// We only support the bk namespace and a well-formed millisecond epoch.
if namespace != bkNamespace {
return
}
millis, err := strconv.ParseInt(data["t"], 10, 64)
if err != nil {
return
}
time := time.Unix(millis/1000, (millis%1000)*1_000_000).UTC()
// One of the formats accepted by the <time> tag:
datetime := time.Format("2006-01-02T15:04:05.999Z")
timeTagImpl.Execute(&b.buf, datetime)
}
// Append a character to our outputbuffer, escaping HTML bits as necessary.
func (b *outputBuffer) appendChar(char rune) {
switch char {
case '&':
b.buf.WriteString("&")
case '\'':
b.buf.WriteString("'")
case '<':
b.buf.WriteString("<")
case '>':
b.buf.WriteString(">")
case '"':
b.buf.WriteString(""")
case '/':
b.buf.WriteString("/")
default:
b.buf.WriteRune(char)
}
}
// asHTML returns the line with HTML formatting.
func (l *screenLine) asHTML() string {
var lineBuf outputBuffer
if data, ok := l.metadata[bkNamespace]; ok {
lineBuf.appendMeta(bkNamespace, data)
}
// tagStack is used as a stack of open tags, so they can be closed in the
// right order. We only have two kinds of tag, so the stack should be tiny,
// but the algorithm can be extended later if needed.
tagStack := make([]int, 0, 2)
const (
tagAnchor = iota
tagSpan
)
// Close tags in the stack, starting at idx. They're closed in the reverse
// order they were opened.
closeFrom := func(idx int) {
for i := len(tagStack) - 1; i >= idx; i-- {
switch tagStack[i] {
case tagAnchor:
lineBuf.closeAnchor()
case tagSpan:
lineBuf.closeStyle()
}
}
tagStack = tagStack[:idx]
}
for x, current := range l.nodes {
// The zero value for node has a plain style and no hyperlink.
var previous node
// If we're past the first node in the line, there is a previous node
// to the left.
if x > 0 {
previous = l.nodes[x-1]
}
// A set of flags for which tags need changing.
tagChanged := []bool{
// The anchor tag needs changing if the link "style" has changed,
// or if they are both links the link URLs are different.
// (Note that the x-1 index into the hyperlinks map returns "".)
tagAnchor: current.style.hyperlink() != previous.style.hyperlink() ||
(current.style.hyperlink() && l.hyperlinks[x-1] != l.hyperlinks[x]),
// The span tag needs changing if the style has changed.
tagSpan: !current.hasSameStyle(previous),
}
// Go forward through the stack of open tags, looking for the first
// tag we need to close (because it changed).
// If none are found, closeFromIdx will be past the end of the stack.
closeFromIdx := len(tagStack)
for i, ot := range tagStack {
if tagChanged[ot] {
closeFromIdx = i
break
}
}
// Close everything from that stack index onwards.
closeFrom(closeFromIdx)
// Now open new tags as needed.
// Open a new anchor tag, if one is not already open and this node is
// hyperlinked.
if !slices.Contains(tagStack, tagAnchor) && current.style.hyperlink() {
lineBuf.appendAnchor(l.hyperlinks[x])
tagStack = append(tagStack, tagAnchor)
}
// Open a new span tag, if one is not already open and this node has
// style.
if !slices.Contains(tagStack, tagSpan) && !current.style.isPlain() {
lineBuf.appendNodeStyle(current)
tagStack = append(tagStack, tagSpan)
}
// Write a standalone element or a rune.
if current.style.element() {
lineBuf.buf.WriteString(l.elements[current.blob].asHTML())
} else {
lineBuf.appendChar(current.blob)
}
}
// Close any that are open, in reverse order that they were opened.
closeFrom(0)
line := strings.TrimRight(lineBuf.buf.String(), " \t")
if line == "" {
return " "
}
return line
}
// asPlain returns the line contents without any added HTML.
func (l *screenLine) asPlain() string {
var buf strings.Builder
for _, node := range l.nodes {
if !node.style.element() {
buf.WriteRune(node.blob)
}
}
return strings.TrimRight(buf.String(), " \t")
}