1package main
2
3import (
4 _ "embed"
5 "fmt"
6 "os"
7 "strings"
8
9 "gioui.org/app"
10 "gioui.org/font/gofont"
11 "gioui.org/io/system"
12 "gioui.org/layout"
13 "gioui.org/op"
14 "gioui.org/text"
15 "gioui.org/unit"
16 "gioui.org/widget/material"
17 "github.com/adrg/frontmatter"
18 "github.com/go-git/go-git/v5"
19 flag "github.com/spf13/pflag"
20 "gopkg.in/yaml.v3"
21)
22
23var (
24 flagHelp *bool = flag.BoolP("help", "h", false, "Show the help message")
25 flagInput *string = flag.StringP("input", "i", "", "Path to input Markdown")
26 flagOutput *string = flag.StringP("output", "o", "", "Path to output PNG")
27 flagMetaSize *int = flag.IntP("metasize", "m", 12, "Size of font for meta information")
28 flagPostTitleSize *int = flag.IntP("posttitlesize", "p", 14, "Size of font for post title")
29 flagSiteTitleSize *int = flag.IntP("sitetitlesize", "s", 10, "Size of font for site title")
30)
31
32//go:embed font.otf
33var fontEmbed []byte
34
35func main() {
36 flag.Parse()
37
38 if *flagHelp {
39 help()
40 os.Exit(0)
41 }
42
43 validateFlags()
44
45 postTitle, postSubtitle, postDate, postContent := getPostInfo(*flagInput)
46 postReadTime := getReadTime(postContent)
47 siteTitle := getSiteTitle()
48 dateEdited := getGitDate(*flagInput)
49
50 // TODO: Render information to image
51 fmt.Println(postTitle, postSubtitle, postDate, postReadTime, siteTitle, dateEdited)
52
53 // fontFace, err := opentype.Parse(fontEmbed)
54 // if err != nil {
55 // fmt.Println("Error: Could not parse font")
56 // fmt.Println(err)
57 // os.Exit(1)
58 // }
59
60 go func() {
61 w := app.NewWindow(app.Title("p2c"), app.Size(unit.Dp(1200), unit.Dp(630)))
62 var ops op.Ops
63 th := material.NewTheme(gofont.Collection())
64 for e := range w.Events() {
65 switch e := e.(type) {
66 case system.FrameEvent:
67 gtx := layout.NewContext(&ops, e)
68 title := material.H3(th, postTitle)
69 title.Alignment = text.Middle
70 title.Layout(gtx)
71 e.Frame(gtx.Ops)
72 }
73 }
74 os.Exit(0)
75 }()
76 app.Main()
77}
78
79// Print help message
80func help() {
81 fmt.Println("\nUsage: p2c [options]")
82 fmt.Println("\nOptions:")
83 flag.PrintDefaults()
84 fmt.Print(`
85example: p2c -i input.md -o output.png
86
87p2c is meant for use with Hugo.
88
89It looks at...
90- The Markdown file's frontmatter fields for
91 - title
92 - subtitle
93 - date
94- The site's config.{toml/yaml/yml} fields for
95 - title (site title)
96- The git history to determine the date the post was last edited.
97
98`)
99}
100
101// Validate flags
102func validateFlags() {
103 if *flagInput == "" {
104 fmt.Println("Error: No input file specified")
105 os.Exit(1)
106 }
107 if *flagOutput == "" {
108 fmt.Println("Error: No output file specified")
109 os.Exit(1)
110 }
111}
112
113// Get the post's title, subtitle, and date
114func getPostInfo(input string) (string, string, string, string) {
115 if _, err := os.Stat(input); os.IsNotExist(err) {
116 fmt.Println("Error: Input file does not exist")
117 fmt.Println(err)
118 os.Exit(1)
119 }
120
121 fileContents, err := os.ReadFile(input)
122 if err != nil {
123 fmt.Println("Error: Could not read input file")
124 fmt.Println(err)
125 os.Exit(1)
126 }
127
128 var fm struct {
129 Title string `yaml:"title"`
130 Subtitle string `yaml:"subtitle"`
131 Date string `yaml:"date"`
132 }
133
134 content, err := frontmatter.Parse(strings.NewReader(string(fileContents)), &fm)
135 if err != nil {
136 fmt.Println("Error: Could not parse frontmatter")
137 fmt.Println(err)
138 os.Exit(1)
139 }
140
141 fm.Date = strings.Split(fm.Date, "T")[0]
142
143 return fm.Title, fm.Subtitle, fm.Date, string(content)
144}
145
146// Get the read time of the post
147func getReadTime(content string) int {
148 wordCount := len(strings.Fields(content))
149 return wordCount / 200
150}
151
152// Get the site's title
153func getSiteTitle() string {
154 validConf := ""
155 confs := []string{"config.toml", "config.yaml", "config.yml"}
156 for _, conf := range confs {
157 if _, err := os.Stat(conf); os.IsNotExist(err) {
158 continue
159 }
160 validConf = conf
161 }
162 if validConf == "" {
163 fmt.Println("Error: No valid config file found")
164 os.Exit(1)
165 }
166
167 var t struct {
168 Title string `yaml:"title"`
169 }
170
171 f, err := os.Open(validConf)
172 if err != nil {
173 fmt.Println("Error: Could not open config file")
174 fmt.Println(err)
175 os.Exit(1)
176 }
177 defer f.Close()
178
179 decoder := yaml.NewDecoder(f)
180 err = decoder.Decode(&t)
181 if err != nil {
182 fmt.Println("Error: Could not parse config file")
183 fmt.Println(err)
184 os.Exit(1)
185 }
186
187 return t.Title
188}
189
190// Get the date the post was last edited
191func getGitDate(input string) string {
192 if _, err := os.Stat(input); os.IsNotExist(err) {
193 fmt.Println("Error: Input file does not exist")
194 os.Exit(1)
195 }
196
197 repo, err := git.PlainOpenWithOptions(".", &git.PlainOpenOptions{DetectDotGit: true})
198 if err != nil {
199 fmt.Println("Error: Could not open git repository")
200 fmt.Println(err)
201 os.Exit(1)
202 }
203
204 commitIter, err := repo.Log(&git.LogOptions{FileName: &input})
205 if err != nil {
206 fmt.Println("Error: Could not get git history")
207 fmt.Println(err)
208 os.Exit(1)
209 }
210
211 commit, err := commitIter.Next()
212 if err != nil {
213 fmt.Println("Error: Could not get git history")
214 fmt.Println(err)
215 os.Exit(1)
216 }
217
218 return commit.Committer.When.Format("2006-01-02")
219}