Add support for branch and leaf bundles

This patch adds support for Hugo's branch and leaf bundles: https://gohugo.io/content-management/page-bundles/

Content directories can now have nested subdirectories, and content from each level will be converted to Gemini. If a subdirectory has a file named index.md (or any index.* file per the footnote on the Hugo page) then it becomes a leaf bundle. For leaf bundles, only the generated index.gmi file will be passed up to parent directories as a page. The rest will be treated as "resources" that are not normally visible to pages in parent folders.

Each parent _index.md receives a list of pages from all nested subdirectories (except for the aforementioned leaf resource pages). This allows for content "rollup" pages. For instance, /series could show all pages under /series/first-season and /series/second-season, while allowing each subfolder to have its own more limited index. This also allows for "clean" URLs, as leaf bundles can be used to make pages that are basically just an index.md page under a named folder. You can then use the trimSuffix function (from sprig) in your gotmpl templates to clean up any links, by stripping "/index.md".

This patch also adds the copying of non-Markdown resource files from content directories, so that bundled resources can be pulled in. Theoretically if someone puts carefully named .gmi files in their content folder then this could cause a name collision, but you'd almost have to try to make that happen, so I didn't put in any checks for it. If this happens then any generated files with the same name are simply overwritten, so there's no serious harm done.

Hugo's leaf bundles support a special front matter attribute, "headless", which if set to true will cause the index.md file to be "invisible" and it will not be passed up to the parent. This is not currently implemented.
This commit is contained in:
mntn 2021-08-17 13:59:11 -04:00 committed by GitHub
parent 0ac0476e62
commit c2f2386a3d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23

View file

@ -31,11 +31,16 @@
// Its content is taken from _index.gmi.md in that dir. If there's no
// matching template or no _index.gmi.md, the index won't be rendered.
//
// Templates for subdirectories are placed in subfolders under top/.
// For example, a template for an index at series/first/_index.gmi.md
// should be placed at top/series/first.gotmpl.
//
// 3. The very top index.gmi is generated from index.gotmpl and
// top-level _index.gmi.
//
// The program will then copy static files from static/ directory to the
// output dir.
// output dir. Page resources (non-Markdown files) will also be copied
// from the content/ directory as-is, without further modification.
//
// Templates are passed the following data:
//
@ -49,13 +54,19 @@
// directory name relative to content dir, and .Content, which is
// rendered from directory's _index.gmi.md.
//
// Directory indices are passed all posts from subdirectories (branch
// and leaf bundles), with the exception of leaf resource pages.
// This allows for roll-up indices.
//
// 3. The top-level index.gmi is passed with the .PostData map whose
// keys are top-level content directories names and values are slices
// over the same post props as specified in 1, and .Content, which is
// rendered from top-level _index.gmi.md.
//
// This program provides some extra template functions, documented in
// templates.go.
// templates.go. Template functions from sprig are also available
// (https://github.com/Masterminds/sprig); see the sprig documentation
// for more details.
//
// One might want to ignore _index.gmi.md files with the following Hugo
// config option in config.toml:
@ -99,8 +110,9 @@ const (
)
var (
tmplNameRegex = regexp.MustCompile(templateBase + `([\w-_ /]+)\.gotmpl`)
topLevelPostRegex = regexp.MustCompile(contentBase + `([\w-_ ]+)/([\w-_ ]+)\.md`)
tmplNameRegex = regexp.MustCompile("^" + templateBase + `([\w-_ /]+)\.gotmpl$`)
leafIndexRegex = regexp.MustCompile("^" + contentBase + `([\w-_ /]+)/index\.[\w]+$`)
pagePathRegex = regexp.MustCompile("^" + contentBase + `([\w-_ /]+)/([\w-_ ]+)\.md$`)
)
var hugoConfigFiles = []string{"config.toml", "config.yaml", "config.json"}
@ -150,6 +162,15 @@ func writeFile(dst string, contents []byte) error {
return nil
}
func hasSubPath(paths []string, path string) bool {
for _, p := range paths {
if strings.HasPrefix(path, p + "/") {
return true
}
}
return false
}
var version = "v0+HEAD"
func main() {
@ -214,6 +235,23 @@ func main() {
}
}
// collect leaf node paths (directories containing an index.* file)
leafIndexPaths := []string{}
if err := filepath.Walk(contentBase, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
if info.IsDir() {
return nil
}
if matches := leafIndexRegex.FindStringSubmatch(path); matches != nil {
leafIndexPaths = append(leafIndexPaths, contentBase + matches[1])
}
return nil
}); err != nil {
panic(err)
}
// render posts to Gemtext and collect top level posts data
posts := make(map[string]*post)
topLevelPosts := make(map[string][]*post)
@ -242,8 +280,22 @@ func main() {
Metadata: metadata,
}
posts[key] = &p
if matches := topLevelPostRegex.FindStringSubmatch(path); matches != nil {
topLevelPosts[matches[1]] = append(topLevelPosts[matches[1]], &p)
if matches := pagePathRegex.FindStringSubmatch(path); matches != nil {
dirs := strings.Split(matches[1], "/")
// only include leaf resources pages in leaf index
if info.Name() != "index.md" && hasSubPath(leafIndexPaths, path) {
topLevelPosts[matches[1]] = append(topLevelPosts[matches[1]], &p)
} else {
// include normal pages in all subdirectory indices
for i, dir := range dirs {
if i > 0 {
dirs[i] = dirs[i - 1] + "/" + dir
}
}
for _, dir := range dirs {
topLevelPosts[dir] = append(topLevelPosts[dir], &p)
}
}
}
return nil
}); err != nil {
@ -341,6 +393,18 @@ func main() {
panic(err)
}
// copy page resources to output dir
if err := filepath.Walk(contentBase, func(p string, info os.FileInfo, err error) error {
if err != nil {
return err
}
if info.IsDir() || strings.HasSuffix(info.Name(), ".md") {
return nil
}
return copyFile(path.Join(outputDir, strings.TrimPrefix(p, contentBase)), p)
}); err != nil {
panic(err)
}
// copy static files to output dir unmodified
if err := filepath.Walk(staticBase, func(p string, info os.FileInfo, err error) error {
if os.IsNotExist(err) {