Recently Written · git

recentlywritten

my writings (done recently)

git clone https://github.com/equwal/recentlywritten

Log | Files | Refs


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 }