commit 970dca3f5852f460c5ea8987d9f3bfb77cb1254f Author: dre Date: Sat Jul 10 21:27:58 2021 +0800 inital testing and ideas in readme diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7e04ad8 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +public +config.toml +hugoext diff --git a/README.md b/README.md new file mode 100644 index 0000000..3957b0f --- /dev/null +++ b/README.md @@ -0,0 +1,78 @@ +# hugoext + +Utility to parse a hugo config file and create the same file structure for content through an +arbitrary output pipe extension. + +Hugo parses primarily markdown files and go templates. The initial motivation for this utility was +to enable the same tools to publish a gemlog version of the same blog to make it accessible through +the Gemini protocol. + +**NOTE**: not many features, this is minimal and only has one use case for now. + +Features +- reads hugo `.toml` file for section output formats +- supports an arbitrary document processor, any program that supports UNIX pipes + +When the selected extension is blank, markdown files will be copied unmodified. + +## Example Use + +Using the [md2gmi](https://github.com/n0x1m/md2gmi) command line utility to convert markdown to +gemtext. Executed from the hugo directory: + +``` +hugoext -ext gmi -pipe md2gmi +``` + +It abides the hugo section config in `[permalinks]` but only uses the content subdirectory to +determine the section. An example section config in hugo looks like this: + +``` +[permalink] +posts = "/posts/:year/:month:day/:filename" +snippets = "/snippets/:filename" +page = ":filename" +``` + +### Installation + +``` +go install github.com/n0x1m/hugoext +``` + +To use the gemini file server and markdown to gemtext converter in the examples below, also install +these: + +``` +go install github.com/n0x1m/md2gmi +go install github.com/n0x1m/gmifs +``` + +### Development + +To test the extension in a similar fashion to the hugo workflow, use a server to host the static +files. Here an example for a Gemlog using [gmifs](https://github.com/n0x1m/gmifs) in a makefile: + +```makefile +serve: + hugoext -ext gmi -pipe md2gmi -serve="gmifs -autoindex" +``` + +hugoext pipes the input through the `md2gmi` extension and spawns `gmifs` to serve the local gemini +directory with auto indexing enabled. + +### Production + +I have a makefile target in my hugo directory to build and publish html and gemtext content: + +```makefile +build: + hugo --minify + hugoext -ext gmi -pipe md2gmi + +publish: build + rsync -a -P --delete ./public/ dre@nox.im/var/www/htdocs/nox.im/ +``` + +The output directory for both hugo and hugoext is `./public`. It's ok to mix the two into the same +file tree as each directory will contain an `index.html` and an `index.gmi` file. diff --git a/main.go b/main.go new file mode 100644 index 0000000..ae97fb6 --- /dev/null +++ b/main.go @@ -0,0 +1,186 @@ +package main + +import ( + "flag" + "fmt" + "log" + "os" + "path" + "path/filepath" + "time" + + "github.com/gohugoio/hugo/config" + "github.com/spf13/afero" +) + +const ( + defaultExt = "gmi" + defaultProcessor = "md2gmi" + defaultSource = "content" + defaultDestination = "public" + defaultConfigPath = "config.toml" + defaultUglyURLs = false + + defaultPermalinkFormat = "/:year/:month/:title/" +) + +func main() { + var ext, processor, source, destination, cfgpath string + var uglyurls bool + + flag.StringVar(&ext, "ext", defaultExt, "ext to look for templates in ./layout") + flag.StringVar(&processor, "proc", defaultProcessor, "processor to pipe markdown content through") + flag.StringVar(&source, "source", defaultSource, "source directory") + flag.StringVar(&destination, "destination", defaultDestination, "output directory") + flag.StringVar(&cfgpath, "config", defaultConfigPath, "hugo config path") + flag.BoolVar(&uglyurls, "ugly-urls", defaultUglyURLs, "use directories with index or .ext files") + flag.Parse() + + osfs := afero.NewOsFs() + cfg, err := config.FromFile(osfs, "config.toml") + if err != nil { + log.Fatal("config from file", err) + } + + permalinks := cfg.GetStringMapString("permalinks") + fmt.Println("permalinks", permalinks) + + fpath := "content/posts/first-post.md" + file, err := os.Open(fpath) + if err != nil { + log.Fatal("open", err) + } + + page, err := ReadFrom(file) + if err != nil { + log.Fatal("read page", err) + } + + fmt.Println(string(page.FrontMatter())) + fmt.Println(string(page.Content())) + meta, err := page.Metadata() + if err != nil { + log.Fatal("read meta", err) + } + c := NewContentFromMeta(meta) + fmt.Println(c) + link, err := pathPattern(permalinks["posts"]).Expand(c) + if err != nil { + log.Fatal("permalink expand", err) + } + fmt.Println(link) + + // test + linkcfg := func(subdir string) string { + format, ok := permalinks[subdir] + if ok { + return format + } + return defaultPermalinkFormat + } + listDirectory(source, linkcfg) +} + +func parse(fullpath string) (*Content, error) { + file, err := os.Open(fullpath) + if err != nil { + return nil, err + } + page, err := ReadFrom(file) + if err != nil { + return nil, err + } + meta, err := page.Metadata() + if err != nil { + return nil, err + } + c := NewContentFromMeta(meta) + fmt.Println(c) + + return c, nil +} + +func listDirectory(fullpath string, linkcfg func(string) string) error { + return filepath.Walk(fullpath, + func(p string, info os.FileInfo, err error) error { + if err != nil { + return err + } + if info.IsDir() { + return nil + } + + rel, err := filepath.Rel(fullpath, p) + if err != nil { + return err + } + + filename := info.Name() + ext := path.Ext(filename) + name := filename[0 : len(filename)-len(ext)] + parent := filepath.Dir(rel) + fmt.Println(p, info.Name(), parent, rel, ext) + c, err := parse(p) + if err != nil { + return err + } + c.Filepath = name + + if parent != "." { + pattern := linkcfg(parent) + link, err := pathPattern(pattern).Expand(c) + if err != nil { + return err + } + c.Permalink = link + } else { + c.Permalink = filename + } + fmt.Println(c) + + return nil + }) +} + +func NewContentFromMeta(meta map[string]interface{}) *Content { + return &Content{ + Title: istring(meta["title"]), + Slug: istring(meta["slug"]), + Categories: istringArr(meta["categories"]), + Date: idate(meta["date"]), + } +} + +func istring(input interface{}) string { + str, ok := input.(string) + if ok { + return str + } + return "" +} + +func idate(input interface{}) time.Time { + str, ok := input.(string) + if !ok { + return time.Now() + + } + t, err := time.Parse(time.RFC3339, str) + if err != nil { + // try just date, or give up + t, err := time.Parse("2006-01-02", str) + if err != nil { + return time.Now() + } + return t + } + return t +} + +func istringArr(input interface{}) []string { + str, ok := input.([]string) + if ok { + return str + } + return nil +}