um/tag/parse.go

124 lines
2.7 KiB
Go

package tag
import (
"errors"
"fmt"
"log"
"os"
"path/filepath"
"regexp"
"strings"
"time"
"github.com/brtholomy/um/go/pipe"
)
const (
// TODO: source in common with last.go
GLOB = "[0-9]*.md"
// The layout string must be a representation of:
// Jan 2 15:04:05 2006 MST
// 1 2 3 4 5 6 -7
DATE_FORMAT = "2006.01.02"
// ^: YYYY.DD.MM$
DATE_REGEXP = `(?m)^\: ([\.0-9]+?)$`
// ^+ tag$
TAG_REGEXP = `(?m)^\+ (.+)$`
)
// regexp.Compile is by far the most expensive operation of ParseContent:
var dateRegexp *regexp.Regexp = regexp.MustCompile(DATE_REGEXP)
var tagRegexp *regexp.Regexp = regexp.MustCompile(TAG_REGEXP)
type Entry struct {
filename string
date time.Time
content string
tags []string
}
func parseHeader(content *string) string {
// returns complete string if not found:
header, _, _ := strings.Cut(*content, "\n\n")
return header
}
func parseTags(content *string) (tags []string) {
res := tagRegexp.FindAllStringSubmatch(*content, -1)
for i := range res {
// group submatch is indexed at 1:
// this shouldn't ever fail if there's a result:
tags = append(tags, res[i][1])
}
return tags
}
func parseDate(content *string) (time.Time, error) {
res := dateRegexp.FindStringSubmatch(*content)
if len(res) < 2 {
return time.Time{}, errors.New("failed to find date string")
}
return time.Parse(DATE_FORMAT, res[1])
}
func parseContent(filename string, content *string) Entry {
base := filepath.Base(filename)
header := parseHeader(content)
date, _ := parseDate(&header)
tags := parseTags(&header)
return Entry{
base,
date,
*content,
tags,
}
}
// reads files from stdin if present, otherwise from the glob pattern:
func getFilelist(glob string) []string {
filelist, err := pipe.GetStdin()
// otherwise get from the glob:
if err != nil {
filelist, err = filepath.Glob(glob)
if err != nil {
log.Fatal(err)
}
}
return filelist
}
// create []Entry representing qualifying files in current directory or from stdin
func entriesGlobOrStdin() []Entry {
filelist := getFilelist(GLOB)
// NOTE: size 0, capacity specified:
entries := make([]Entry, 0, len(filelist))
for _, f := range filelist {
dat, err := os.ReadFile(f)
if err != nil {
log.Fatal(fmt.Errorf("gag: error opening file: %s\n%w", f, err))
}
s := string(dat)
e := parseContent(f, &s)
entries = append(entries, e)
}
return entries
}
// maps tags to a set of filenames
func makeTagmap(entries []Entry) map[string]Set {
tagmap := map[string]Set{}
for _, e := range entries {
for _, tag := range e.tags {
// allocate submap if necessary:
if _, ok := tagmap[tag]; !ok {
tagmap[tag] = Set{}
}
tagmap[tag].Add(e.filename)
}
}
return tagmap
}