coverage~bigbes/sr-ht-spec64cae3afdoc/log.go

Coverage
100.0% 25/25 statements
Δ
+0.0
Blob
f428e8c
Uncovered nothing — every instrumented line ran
1 package doc
2
3 import (
4 "fmt"
5 "regexp"
6 "strings"
7 )
8
9 // LogSection is the Section every log entry carries in the index. It is not a
10 // real directory: the entries all come out of one document. Default search
11 // excludes it, so a query returns the document describing a thing rather than
12 // the log entry paraphrasing it, and an explicit filter reaches it.
13 const LogSection = "log"
14
15 // entryHeadRe matches an activity log's entry header:
16 // "## [YYYY-MM-DD] action | Title".
17 var entryHeadRe = regexp.MustCompile(`^##\s+\[(\d{4}-\d{2}-\d{2})\]\s+([a-z]+)\s*\|\s*(.*)$`)
18
19 // LogEntry is one dated entry of an activity log (a document marked
20 // `type: log`), indexed as its own document.
21 //
22 // Indexing such a log as a single document is the wrong shape twice over: it is
23 // a large body of text summarising other documents, so it produces chunks that
24 // are near duplicates of what they describe and compete with them for the same
25 // queries, and a hit anywhere in the file resolves to the whole file. Split by
26 // entry, each piece is the size of the thing it describes and answers the
27 // question a log is uniquely good for — when something landed, what changed in
28 // a month, what came out of one session.
29 type LogEntry struct {
30 ID string // "log#<date>-<n>", unique even when a date repeats
31 Date string // YYYY-MM-DD
32 Action string // ingest | query | lint | update
33 Title string
34 Body string // entry text, header line excluded
35 Anchor string // heading anchor within /log
36 }
37
38 // SplitLog parses an activity log's body into entries. Text before the first
39 // entry header (the file's own title) is dropped. A file that matches no header
40 // at all yields no entries, and the caller keeps treating it as one ordinary
41 // document.
42 2 func SplitLog(body []byte) []LogEntry {
43 2 lines := strings.Split(string(body), "\n")
44 2
45 2 var (
46 2 entries []LogEntry
47 2 cur *LogEntry
48 2 buf []string
49 2 perDate = map[string]int{}
50 2 )
51 6 flush := func() {
52 6 if cur == nil {
53 2 return
54 2 }
55 4 cur.Body = strings.TrimSpace(strings.Join(buf, "\n"))
56 4 entries = append(entries, *cur)
57 4 cur, buf = nil, nil
58 }
59
60 2 inFence := false
61 21 for _, line := range lines {
62 21 trimmed := strings.TrimSpace(line)
63 21 if strings.HasPrefix(trimmed, "```") || strings.HasPrefix(trimmed, "~~~") {
64 2 inFence = !inFence
65 2 }
66 21 if !inFence {
67 19 if m := entryHeadRe.FindStringSubmatch(line); m != nil {
68 4 flush()
69 4 date, action, title := m[1], m[2], strings.TrimSpace(m[3])
70 4 perDate[date]++
71 4 cur = &LogEntry{
72 4 ID: fmt.Sprintf("log#%s-%d", date, perDate[date]),
73 4 Date: date,
74 4 Action: action,
75 4 Title: title,
76 4 Anchor: fmt.Sprintf("e-%s-%d", date, perDate[date]),
77 4 }
78 4 continue
79 }
80 }
81 17 if cur != nil {
82 11 buf = append(buf, line)
83 11 }
84 }
85 2 flush()
86 2 return entries
87 }
88
89 // SearchText is what the keyword index stores for an entry: the header fields
90 // followed by the body, with wikilink brackets left in place — the renderer is
91 // not involved here, and the bracketed names are still the words a reader would
92 // search for.
93 1 func (e LogEntry) SearchText() string {
94 1 return e.Date + " " + e.Action + " " + e.Title + "\n" + e.Body
95 1 }