~bigbes/sr-ht-spec

ref: 7f779fef12194d49b9ce97ad4e2a80af1c3d6358 sr-ht-spec/doc/log.go -rw-r--r-- 3.0 KiB
7f779fef — Eugene Blikh feat(web): review queue — inbox + policy-merged digest (Phase 4) 26 days ago
                                                                                
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
package doc

import (
	"fmt"
	"regexp"
	"strings"
)

// LogSection is the Section every log entry carries in the index. It is not a
// real directory: the entries all come out of one document. Default search
// excludes it, so a query returns the document describing a thing rather than
// the log entry paraphrasing it, and an explicit filter reaches it.
const LogSection = "log"

// entryHeadRe matches an activity log's entry header:
// "## [YYYY-MM-DD] action | Title".
var entryHeadRe = regexp.MustCompile(`^##\s+\[(\d{4}-\d{2}-\d{2})\]\s+([a-z]+)\s*\|\s*(.*)$`)

// LogEntry is one dated entry of an activity log (a document marked
// `type: log`), indexed as its own document.
//
// Indexing such a log as a single document is the wrong shape twice over: it is
// a large body of text summarising other documents, so it produces chunks that
// are near duplicates of what they describe and compete with them for the same
// queries, and a hit anywhere in the file resolves to the whole file. Split by
// entry, each piece is the size of the thing it describes and answers the
// question a log is uniquely good for — when something landed, what changed in
// a month, what came out of one session.
type LogEntry struct {
	ID     string // "log#<date>-<n>", unique even when a date repeats
	Date   string // YYYY-MM-DD
	Action string // ingest | query | lint | update
	Title  string
	Body   string // entry text, header line excluded
	Anchor string // heading anchor within /log
}

// SplitLog parses an activity log's body into entries. Text before the first
// entry header (the file's own title) is dropped. A file that matches no header
// at all yields no entries, and the caller keeps treating it as one ordinary
// document.
func SplitLog(body []byte) []LogEntry {
	lines := strings.Split(string(body), "\n")

	var (
		entries []LogEntry
		cur     *LogEntry
		buf     []string
		perDate = map[string]int{}
	)
	flush := func() {
		if cur == nil {
			return
		}
		cur.Body = strings.TrimSpace(strings.Join(buf, "\n"))
		entries = append(entries, *cur)
		cur, buf = nil, nil
	}

	inFence := false
	for _, line := range lines {
		trimmed := strings.TrimSpace(line)
		if strings.HasPrefix(trimmed, "```") || strings.HasPrefix(trimmed, "~~~") {
			inFence = !inFence
		}
		if !inFence {
			if m := entryHeadRe.FindStringSubmatch(line); m != nil {
				flush()
				date, action, title := m[1], m[2], strings.TrimSpace(m[3])
				perDate[date]++
				cur = &LogEntry{
					ID:     fmt.Sprintf("log#%s-%d", date, perDate[date]),
					Date:   date,
					Action: action,
					Title:  title,
					Anchor: fmt.Sprintf("e-%s-%d", date, perDate[date]),
				}
				continue
			}
		}
		if cur != nil {
			buf = append(buf, line)
		}
	}
	flush()
	return entries
}

// SearchText is what the keyword index stores for an entry: the header fields
// followed by the body, with wikilink brackets left in place — the renderer is
// not involved here, and the bracketed names are still the words a reader would
// search for.
func (e LogEntry) SearchText() string {
	return e.Date + " " + e.Action + " " + e.Title + "\n" + e.Body
}