main.go

  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}