internal/ui/view.go
1
package ui
3
import (
4
"net/url"
5
"strings"
7
"github.com/koment-dev/koment/internal/anchor"
8
"github.com/koment-dev/koment/internal/application"
9
"github.com/koment-dev/koment/internal/store"
10
)
12
const sourceURL = "https://github.com/koment-dev/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
Title string
115
Status anchor.Status
116
Line int
117
Body []string
118
Created string
119
Excerpt string
120
Stale bool
121
Warning string
122
}
124
var statusOrder = []anchor.Status{
125
anchor.StatusOK, anchor.StatusAmbiguous, anchor.StatusDrifted, anchor.StatusOrphaned,
126
}
128
func build(repositorySnapshot *application.RepositorySnapshot, requested string, how links) (*view, error) {
129
if len(repositorySnapshot.Files) == 0 {
130
return &view{
131
Empty: true, Stylesheet: how.stylesheet, Script: how.script,
132
Home: how.home, LogoSVG: how.logoSVG, LogoPNG: how.logoPNG, SourceURL: sourceURL,
133
}, nil
134
}
136
current := requested
137
if current == "" {
138
current = repositorySnapshot.Files[0].Path
139
}
141
built := &view{
142
Current: current,
143
Home: how.home,
144
Stylesheet: how.stylesheet,
145
Script: how.script,
146
LogoSVG: how.logoSVG,
147
LogoPNG: how.logoPNG,
148
SourceURL: sourceURL,
149
}
150
counts := map[anchor.Status]int{}
151
listed := make([]entry, 0, len(repositorySnapshot.Files))
153
for _, file := range repositorySnapshot.Files {
154
worst := anchor.StatusOK
155
var searchable strings.Builder
156
searchable.WriteString(file.Path)
157
for _, annotation := range file.Annotations {
158
counts[annotation.Status]++
159
built.Total++
160
if statusSeverity[annotation.Status] > statusSeverity[worst] {
161
worst = annotation.Status
162
}
163
searchable.WriteString("\n" + string(annotation.Record.Spec.Type) + "\n" + annotation.Record.Headline() + "\n" + annotation.Record.Spec.Body + "\n" + annotation.Record.Spec.Author.Name)
164
}
166
listed = append(listed, entry{
167
Path: file.Path,
168
Name: baseName(file.Path),
169
Href: how.file(file.Path),
170
Count: len(file.Annotations),
171
Worst: worst,
172
Current: file.Path == current,
173
Search: strings.ToLower(searchable.String()),
174
})
176
if file.Path == current {
177
built.File = buildFile(file)
178
}
179
}
181
if built.File == nil {
182
built.NotFound = true
183
}
184
built.Tally = tallyOf(counts)
185
built.Tree, built.Loose = buildTree(listed, current)
186
return built, nil
187
}
189
func baseName(file string) string {
190
if cut := strings.LastIndex(file, "/"); cut >= 0 {
191
return file[cut+1:]
192
}
193
return file
194
}
196
func tallyOf(counts map[anchor.Status]int) []tallyEntry {
197
var tally []tallyEntry
198
for _, status := range statusOrder {
199
if counts[status] > 0 {
200
tally = append(tally, tallyEntry{Status: status, Count: counts[status]})
201
}
202
}
203
return tally
204
}
206
func buildFile(file application.FileSnapshot) *fileView {
207
built := &fileView{Path: file.Path}
208
if !file.Exists {
209
built.Missing = true
210
for _, annotation := range file.Annotations {
211
built.Detached = append(built.Detached, describe(annotation))
212
}
213
return built
214
}
216
marked := map[int]anchor.Status{}
217
for _, annotation := range file.Annotations {
218
described := describe(annotation)
219
if annotation.Line == 0 {
220
built.Detached = append(built.Detached, described)
221
continue
222
}
223
built.Notes = append(built.Notes, described)
224
worst, seen := marked[annotation.Line]
225
if !seen || statusSeverity[annotation.Status] > statusSeverity[worst] {
226
marked[annotation.Line] = annotation.Status
227
}
228
}
230
for i, text := range strings.Split(strings.TrimSuffix(string(file.Content), "\n"), "\n") {
231
number := i + 1
232
built.Lines = append(built.Lines, line{Number: number, Text: text, Marker: marked[number]})
233
}
234
return built
235
}
237
func describe(annotation application.AnnotationView) note {
238
stale := annotation.Status.IsFailure()
239
return note{
240
ID: annotation.Record.Metadata.ID,
241
Kind: string(annotation.Record.Spec.Type),
242
Status: annotation.Status,
243
Line: annotation.Line,
244
Title: annotation.Record.Headline(),
245
Body: store.Paragraphs(annotation.Record.Spec.Body),
246
Created: annotation.Record.Metadata.Created.Format("2006-01-02"),
247
Excerpt: annotation.Record.Spec.Anchor.Excerpt,
248
Stale: stale,
249
Warning: annotation.Warning,
250
}
251
}