internal/ui/view.go
1
package ui
3
import (
4
"net/url"
5
"strings"
7
"github.com/janpuc/koment/internal/anchor"
8
"github.com/janpuc/koment/internal/application"
9
"github.com/janpuc/koment/internal/store"
10
)
12
const sourceURL = "https://github.com/janpuc/koment"
14
type view struct {
15
Total int
16
Tally []tallyEntry
17
Tree []treeNode
18
Loose []entry
19
Repositories []repositoryLink
20
Repository string
21
Current string
22
File *fileView
23
Empty bool
24
NotFound bool
25
Home string
26
Stylesheet string
27
Script string
28
LogoSVG string
29
LogoPNG string
30
SourceURL string
31
Snapshot *snapshot
32
WriteToken string
33
CanWrite bool
34
CreatedID string
35
WriteWarning string
36
ReviewURL string
37
}
39
type snapshot struct {
40
Commit string
41
CommitURL string
42
Banner string
43
BannerHref string
44
}
46
type repositoryLink struct {
47
ID string
48
Name string
49
Href string
50
Current bool
51
}
53
type links struct {
54
file func(target string) string
55
home string
56
stylesheet string
57
script string
58
logoSVG string
59
logoPNG string
60
}
62
func servedLinks(repositoryID string) links {
63
base := repositoryPrefix + repositoryID + "/"
64
return links{
65
file: func(target string) string { return base + "f/" + escapedFilePath(target) },
66
home: base,
67
stylesheet: "/assets/style.css",
68
script: "/assets/koment.js",
69
logoSVG: "/assets/koment-logo.svg",
70
logoPNG: "/assets/koment-logo.png",
71
}
72
}
74
func escapedFilePath(file string) string {
75
parts := strings.Split(file, "/")
76
for index, part := range parts {
77
parts[index] = url.PathEscape(part)
78
}
79
return strings.Join(parts, "/")
80
}
82
type tallyEntry struct {
83
Status anchor.Status
84
Count int
85
}
87
type entry struct {
88
Path string
89
Name string
90
Href string
91
Count int
92
Worst anchor.Status
93
Current bool
94
Search string
95
}
97
type fileView struct {
98
Path string
99
Lines []line
100
Notes []note
101
Detached []note
102
Missing bool
103
}
105
type line struct {
106
Number int
107
Text string
108
Marker anchor.Status
109
}
111
type note struct {
112
ID string
113
Kind string
114
Status anchor.Status
115
Line int
116
Body []string
117
Created string
118
Excerpt string
119
Stale bool
120
Warning string
121
}
123
var statusOrder = []anchor.Status{
124
anchor.StatusOK, anchor.StatusMoved, anchor.StatusAmbiguous, anchor.StatusDrifted, anchor.StatusOrphaned,
125
}
127
func build(repositorySnapshot *application.RepositorySnapshot, requested string, how links) (*view, error) {
128
if len(repositorySnapshot.Files) == 0 {
129
return &view{
130
Empty: true, Stylesheet: how.stylesheet, Script: how.script,
131
Home: how.home, LogoSVG: how.logoSVG, LogoPNG: how.logoPNG, SourceURL: sourceURL,
132
}, nil
133
}
135
current := requested
136
if current == "" {
137
current = repositorySnapshot.Files[0].Path
138
}
140
built := &view{
141
Current: current,
142
Home: how.home,
143
Stylesheet: how.stylesheet,
144
Script: how.script,
145
LogoSVG: how.logoSVG,
146
LogoPNG: how.logoPNG,
147
SourceURL: sourceURL,
148
}
149
counts := map[anchor.Status]int{}
150
listed := make([]entry, 0, len(repositorySnapshot.Files))
152
for _, file := range repositorySnapshot.Files {
153
worst := anchor.StatusOK
154
var searchable strings.Builder
155
searchable.WriteString(file.Path)
156
for _, annotation := range file.Annotations {
157
counts[annotation.Status]++
158
built.Total++
159
if statusSeverity[annotation.Status] > statusSeverity[worst] {
160
worst = annotation.Status
161
}
162
searchable.WriteString("\n" + string(annotation.Record.Kind) + "\n" + annotation.Record.Body + "\n" + annotation.Record.Author.Name)
163
}
165
listed = append(listed, entry{
166
Path: file.Path,
167
Name: baseName(file.Path),
168
Href: how.file(file.Path),
169
Count: len(file.Annotations),
170
Worst: worst,
171
Current: file.Path == current,
172
Search: strings.ToLower(searchable.String()),
173
})
175
if file.Path == current {
176
built.File = buildFile(file)
177
}
178
}
180
if built.File == nil {
181
built.NotFound = true
182
}
183
built.Tally = tallyOf(counts)
184
built.Tree, built.Loose = buildTree(listed, current)
185
return built, nil
186
}
188
func baseName(file string) string {
189
if cut := strings.LastIndex(file, "/"); cut >= 0 {
190
return file[cut+1:]
191
}
192
return file
193
}
195
func tallyOf(counts map[anchor.Status]int) []tallyEntry {
196
var tally []tallyEntry
197
for _, status := range statusOrder {
198
if counts[status] > 0 {
199
tally = append(tally, tallyEntry{Status: status, Count: counts[status]})
200
}
201
}
202
return tally
203
}
205
func buildFile(file application.FileSnapshot) *fileView {
206
built := &fileView{Path: file.Path}
207
if !file.Exists {
208
built.Missing = true
209
for _, annotation := range file.Annotations {
210
built.Detached = append(built.Detached, describe(annotation))
211
}
212
return built
213
}
215
marked := map[int]anchor.Status{}
216
for _, annotation := range file.Annotations {
217
described := describe(annotation)
218
if annotation.Line == 0 {
219
built.Detached = append(built.Detached, described)
220
continue
221
}
222
built.Notes = append(built.Notes, described)
223
worst, seen := marked[annotation.Line]
224
if !seen || statusSeverity[annotation.Status] > statusSeverity[worst] {
225
marked[annotation.Line] = annotation.Status
226
}
227
}
229
for i, text := range strings.Split(strings.TrimSuffix(string(file.Content), "\n"), "\n") {
230
number := i + 1
231
built.Lines = append(built.Lines, line{Number: number, Text: text, Marker: marked[number]})
232
}
233
return built
234
}
236
func describe(annotation application.AnnotationView) note {
237
stale := annotation.Status.IsFailure()
238
return note{
239
ID: annotation.Record.ID,
240
Kind: string(annotation.Record.Kind),
241
Status: annotation.Status,
242
Line: annotation.Line,
243
Body: store.Paragraphs(annotation.Record.Body),
244
Created: annotation.Record.Created.Format("2006-01-02"),
245
Excerpt: annotation.Record.Anchor.Excerpt,
246
Stale: stale,
247
Warning: annotation.Warning,
248
}
249
}