1package main
2
3import (
4 "embed"
5 "fmt"
6 "os"
7 "strings"
8
9 "gioui.org/app"
10 "gioui.org/font/opentype"
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 fonts
33var fontEmbed embed.FS
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.Printf(`Title: %s
52Subtitle: %s
53Read Time: %d minutes
54Date Published: %s
55Date Edited: %s
56Site Title: %s
57`, postTitle, postSubtitle, postReadTime, postDate, dateEdited, siteTitle)
58
59 collection := fontCollection()
60
61 go func() {
62 w := app.NewWindow(app.Title("p2c"), app.Size(unit.Dp(1200), unit.Dp(630)))
63 var ops op.Ops
64 th := material.NewTheme(collection)
65 for e := range w.Events() {
66 switch e := e.(type) {
67 case system.FrameEvent:
68 gtx := layout.NewContext(&ops, e)
69
70 title := material.LabelStyle{
71 Font: text.Font{Typeface: "Primary font", Variant: "", Style: text.Regular, Weight: text.Bold},
72 Alignment: text.Middle,
73 Text: postTitle,
74 TextSize: unit.Sp(float32(*flagPostTitleSize)),
75 }
76 title.Layout(gtx)
77
78 rTime := material.Body1(th, fmt.Sprint("Reading Time: ", postReadTime))
79 rTime.Alignment = text.Middle
80 rTime.Layout(gtx)
81
82 // pDate := material.Body1(th, fmt.Sprint("Published: ", postDate))
83 // pDate.Alignment = text.Middle
84 // pDate.Layout(gtx)
85
86 // eDate := material.Body1(th, fmt.Sprint("Last Edited: ", dateEdited))
87 // eDate.Alignment = text.Middle
88 // eDate.Layout(gtx)
89
90 // sTitle := material.Body1(th, siteTitle)
91 // sTitle.Alignment = text.Middle
92 // sTitle.Layout(gtx)
93
94 e.Frame(gtx.Ops)
95 }
96 }
97 os.Exit(0)
98 }()
99 app.Main()
100}
101
102func fontCollection() []text.FontFace {
103 regularFaceBytes, err := fontEmbed.ReadFile("fonts/regular.otf")
104 if err != nil {
105 fmt.Println("Error: Could not read regular font")
106 fmt.Println(err)
107 os.Exit(1)
108 }
109 regularFace, err := opentype.Parse(regularFaceBytes)
110 if err != nil {
111 fmt.Println("Error: Could not parse regular font")
112 fmt.Println(err)
113 os.Exit(1)
114 }
115
116 boldFaceBytes, err := fontEmbed.ReadFile("fonts/bold.otf")
117 if err != nil {
118 fmt.Println("Error: Could not read bold font")
119 fmt.Println(err)
120 os.Exit(1)
121 }
122 boldFace, err := opentype.Parse(boldFaceBytes)
123 if err != nil {
124 fmt.Println("Error: Could not parse bold font")
125 fmt.Println(err)
126 os.Exit(1)
127 }
128
129 italicFaceBytes, err := fontEmbed.ReadFile("fonts/italic.otf")
130 if err != nil {
131 fmt.Println("Error: Could not read italic font")
132 fmt.Println(err)
133 os.Exit(1)
134 }
135 italicFace, err := opentype.Parse(italicFaceBytes)
136 if err != nil {
137 fmt.Println("Error: Could not parse italic font")
138 fmt.Println(err)
139 os.Exit(1)
140 }
141
142 boldItalicFaceBytes, err := fontEmbed.ReadFile("fonts/bold-italic.otf")
143 if err != nil {
144 fmt.Println("Error: Could not read bold italic font")
145 fmt.Println(err)
146 os.Exit(1)
147 }
148 boldItalicFace, err := opentype.Parse(boldItalicFaceBytes)
149 if err != nil {
150 fmt.Println("Error: Could not parse bold italic font")
151 fmt.Println(err)
152 os.Exit(1)
153 }
154
155 return []text.FontFace{
156 {
157 Font: text.Font{
158 Typeface: "Primary font",
159 Variant: "",
160 Style: text.Regular,
161 Weight: text.Normal,
162 },
163 Face: regularFace,
164 },
165 {
166 Font: text.Font{
167 Typeface: "Primary font",
168 Variant: "",
169 Style: text.Regular,
170 Weight: text.Bold,
171 },
172 Face: boldFace,
173 },
174 {
175 Font: text.Font{
176 Typeface: "Primary font",
177 Variant: "",
178 Style: text.Italic,
179 Weight: text.Normal,
180 },
181 Face: italicFace,
182 },
183 {
184 Font: text.Font{
185 Typeface: "Primary font",
186 Variant: "",
187 Style: text.Italic,
188 Weight: text.Bold,
189 },
190 Face: boldItalicFace,
191 },
192 }
193}
194
195// Print help message
196func help() {
197 fmt.Println("\nUsage: p2c [options]")
198 fmt.Println("\nOptions:")
199 flag.PrintDefaults()
200 fmt.Print(`
201example: p2c -i input.md -o output.png
202
203p2c is meant for use with Hugo.
204
205It looks at...
206- The Markdown file's frontmatter fields for
207 - title
208 - subtitle
209 - date
210- The site's config.{toml/yaml/yml} fields for
211 - title (site title)
212- The git history to determine the date the post was last edited.
213
214`)
215}
216
217// Validate flags
218func validateFlags() {
219 if *flagInput == "" {
220 fmt.Println("Error: No input file specified")
221 os.Exit(1)
222 }
223 if *flagOutput == "" {
224 fmt.Println("Error: No output file specified")
225 os.Exit(1)
226 }
227}
228
229// Get the post's title, subtitle, and date
230func getPostInfo(input string) (string, string, string, string) {
231 if _, err := os.Stat(input); os.IsNotExist(err) {
232 fmt.Println("Error: Input file does not exist")
233 fmt.Println(err)
234 os.Exit(1)
235 }
236
237 fileContents, err := os.ReadFile(input)
238 if err != nil {
239 fmt.Println("Error: Could not read input file")
240 fmt.Println(err)
241 os.Exit(1)
242 }
243
244 var fm struct {
245 Title string `yaml:"title"`
246 Subtitle string `yaml:"subtitle"`
247 Date string `yaml:"date"`
248 }
249
250 content, err := frontmatter.Parse(strings.NewReader(string(fileContents)), &fm)
251 if err != nil {
252 fmt.Println("Error: Could not parse frontmatter")
253 fmt.Println(err)
254 os.Exit(1)
255 }
256
257 fm.Date = strings.Split(fm.Date, "T")[0]
258
259 return fm.Title, fm.Subtitle, fm.Date, string(content)
260}
261
262// Get the read time of the post
263func getReadTime(content string) int {
264 wordCount := len(strings.Fields(content))
265 return wordCount / 200
266}
267
268// Get the site's title
269func getSiteTitle() string {
270 validConf := ""
271 confs := []string{"config.toml", "config.yaml", "config.yml"}
272 for _, conf := range confs {
273 if _, err := os.Stat(conf); os.IsNotExist(err) {
274 continue
275 }
276 validConf = conf
277 }
278 if validConf == "" {
279 fmt.Println("Error: No valid config file found")
280 os.Exit(1)
281 }
282
283 var t struct {
284 Title string `yaml:"title"`
285 }
286
287 f, err := os.Open(validConf)
288 if err != nil {
289 fmt.Println("Error: Could not open config file")
290 fmt.Println(err)
291 os.Exit(1)
292 }
293 defer f.Close()
294
295 decoder := yaml.NewDecoder(f)
296 err = decoder.Decode(&t)
297 if err != nil {
298 fmt.Println("Error: Could not parse config file")
299 fmt.Println(err)
300 os.Exit(1)
301 }
302
303 return t.Title
304}
305
306// Get the date the post was last edited
307func getGitDate(input string) string {
308 if _, err := os.Stat(input); os.IsNotExist(err) {
309 fmt.Println("Error: Input file does not exist")
310 os.Exit(1)
311 }
312
313 repo, err := git.PlainOpenWithOptions(".", &git.PlainOpenOptions{DetectDotGit: true})
314 if err != nil {
315 fmt.Println("Error: Could not open git repository")
316 fmt.Println(err)
317 os.Exit(1)
318 }
319
320 commitIter, err := repo.Log(&git.LogOptions{FileName: &input})
321 if err != nil {
322 fmt.Println("Error: Could not get git history")
323 fmt.Println(err)
324 os.Exit(1)
325 }
326
327 commit, err := commitIter.Next()
328 if err != nil {
329 fmt.Println("Error: Could not get git history")
330 fmt.Println(err)
331 os.Exit(1)
332 }
333
334 return commit.Committer.When.Format("2006-01-02")
335}