| 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 |
} |