main.go

  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}