accounts/posts.go (2477 bytes)
1 package main 2 3 import ( 4 "bufio" 5 "fmt" 6 "io" 7 "os" 8 "path/filepath" 9 "regexp" 10 "strings" 11 ) 12 13 // Post is one line of the post index that build.sh writes. 14 type Post struct { 15 Date string // YYYY-MM-DD, or empty 16 Slug string 17 Tags []string 18 Title string 19 } 20 21 // validSlug accepts only the file names that build.sh makes. The check 22 // keeps a bad index line from reading a file outside postdata/. 23 var validSlug = regexp.MustCompile(`^[A-Za-z0-9_-][A-Za-z0-9._-]*$`) 24 25 // parseIndex reads postdata/index.tsv. Each line has four fields that 26 // tabs separate: date, slug, tags, title. The lines are newest first. 27 func parseIndex(r io.Reader) ([]Post, error) { 28 var posts []Post 29 sc := bufio.NewScanner(r) 30 for n := 1; sc.Scan(); n++ { 31 line := strings.TrimRight(sc.Text(), "\r") 32 if line == "" { 33 continue 34 } 35 f := strings.SplitN(line, "\t", 4) 36 if len(f) != 4 || !validSlug.MatchString(f[1]) { 37 return nil, fmt.Errorf("index line %d is not valid", n) 38 } 39 posts = append(posts, Post{ 40 Date: f[0], 41 Slug: f[1], 42 Tags: splitTags(f[2]), 43 Title: f[3], 44 }) 45 } 46 return posts, sc.Err() 47 } 48 49 // splitTags reads a front matter tags value such as "articles" or 50 // "lisp, unix". 51 func splitTags(s string) []string { 52 return strings.FieldsFunc(s, func(r rune) bool { 53 return r == ',' || r == ' ' || r == '\t' 54 }) 55 } 56 57 func loadPosts(siteDir string) ([]Post, error) { 58 f, err := os.Open(filepath.Join(siteDir, "postdata", "index.tsv")) 59 if err != nil { 60 return nil, err 61 } 62 defer f.Close() 63 return parseIndex(f) 64 } 65 66 // loadBody returns the HTML body of a post. A missing body gives an 67 // empty string, because a feed item without a body is still useful. 68 func loadBody(siteDir, slug string) string { 69 b, err := os.ReadFile(filepath.Join(siteDir, "postdata", slug+".html")) 70 if err != nil { 71 return "" 72 } 73 return string(b) 74 } 75 76 // allTags returns each tag that a post uses, in the order of first use. 77 func allTags(posts []Post) []string { 78 seen := map[string]bool{} 79 var tags []string 80 for _, p := range posts { 81 for _, t := range p.Tags { 82 if !seen[t] { 83 seen[t] = true 84 tags = append(tags, t) 85 } 86 } 87 } 88 return tags 89 } 90 91 // wanted tells if a user who muted the tags in muted gets post p. A post 92 // without tags always goes out. A post with tags goes out if one or more 93 // of its tags is not muted. 94 func wanted(p Post, muted map[string]bool) bool { 95 if len(p.Tags) == 0 { 96 return true 97 } 98 for _, t := range p.Tags { 99 if !muted[t] { 100 return true 101 } 102 } 103 return false 104 }