-
-
Save a-h/194b9b1bc9414d15c38aa1599c4ae17e to your computer and use it in GitHub Desktop.
Gemini generator
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package main | |
import ( | |
"encoding/json" | |
"fmt" | |
"io/fs" | |
"io/ioutil" | |
"os" | |
"os/exec" | |
"path" | |
"path/filepath" | |
"sort" | |
"strings" | |
"time" | |
) | |
const indexHeader = `# Index Header | |
` | |
const indexFooter = `` | |
const pageHeader = `# Page Header | |
=> gemini://your_address home | |
` | |
func pageFooter(root string, p Post, all Posts) string { | |
var sb = new(strings.Builder) | |
sb.WriteString("\n\n") | |
index := indexOf(p, all) | |
sb.WriteString(fmt.Sprintf("# More\n\n")) | |
if index > 0 { | |
sb.WriteString(fmt.Sprintf("## Next\n\n")) | |
sb.WriteString(fmt.Sprintf("=> %s %s\n\n", all[index-1].Metadata.URL, all[index-1].Metadata.Title)) | |
} | |
if index < len(all)-1 { | |
sb.WriteString(fmt.Sprintf("## Previous\n\n")) | |
sb.WriteString(fmt.Sprintf("=> %s %s\n\n", all[index+1].Metadata.URL, all[index+1].Metadata.Title)) | |
} | |
sb.WriteString(fmt.Sprintf("## Home\n\n")) | |
sb.WriteString("=> gemini://your_address home\n") | |
return sb.String() | |
} | |
func indexOf(p Post, all Posts) int { | |
for i := 0; i < len(all); i++ { | |
if all[i].FileName == p.FileName { | |
return i | |
} | |
} | |
return -1 | |
} | |
func main() { | |
source := "posts" | |
target := "dist" | |
// Clear out the target. | |
exitOnError("failed to delete dist directory", exec.Command("rm", "-rf", target).Run()) | |
exitOnError("failed to copy to dist directory", exec.Command("cp", "-r", source, target).Run()) | |
exitOnError("failed to remove .DS_Store files", exec.Command("find", target, "-name", ".DS_Store", "-delete").Run()) | |
// Get the posts from the dist directory, because we're going to use that as the working directory. | |
posts, err := getPosts(target) | |
exitOnError("failed to get posts", err) | |
// Write out the index. | |
var index = new(strings.Builder) | |
index.WriteString(indexHeader) | |
var year int | |
for _, p := range posts { | |
if p.Metadata.Title == "" { | |
fmt.Printf("WARN: no metadata for %q\n", p.FileName) | |
continue | |
} | |
if p.Metadata.Date.Year() != year { | |
year = p.Metadata.Date.Year() | |
index.WriteString(fmt.Sprintf("## %d\n\n", year)) | |
} | |
index.WriteString(fmt.Sprintf("### %s\n\n", p.Metadata.Title)) | |
index.WriteString(fmt.Sprintf("=> %s\n\n", p.Metadata.URL)) | |
// Rewrite all the gemini files to add a header and footer. | |
header := pageHeader | |
footer := pageFooter(target, p, posts) | |
exitOnError("failed to add header and footer", addHeaderAndFooter(p.FileName, header, footer)) | |
} | |
index.WriteString(indexFooter) | |
exitOnError("failed to write index", os.WriteFile(path.Join(target, "./index.gmi"), []byte(index.String()), os.ModePerm)) | |
} | |
func addHeaderAndFooter(to, header, footer string) error { | |
existing, err := ioutil.ReadFile(to) | |
if err != nil { | |
return err | |
} | |
f, err := os.Create(to) | |
if err != nil { | |
return err | |
} | |
_, err = f.WriteString(header) | |
if err != nil { | |
return err | |
} | |
_, err = f.Write([]byte(strings.TrimSpace(string(existing)))) | |
if err != nil { | |
return err | |
} | |
_, err = f.WriteString(footer) | |
if err != nil { | |
return err | |
} | |
return nil | |
} | |
func exitOnError(msg string, err error) { | |
if err != nil { | |
fmt.Println(msg+":", err) | |
os.Exit(1) | |
} | |
} | |
type Posts []Post | |
type Post struct { | |
FileName string | |
Metadata Metadata | |
} | |
type Metadata struct { | |
Date time.Time `json:"date"` | |
Title string `json:"title"` | |
URL string `json:"url"` | |
Tags []string `json:"tags"` | |
} | |
func getPosts(src string) (posts Posts, err error) { | |
var geminiFiles []string | |
filepath.Walk(src, func(currentPath string, info fs.FileInfo, err error) error { | |
if path.Dir(currentPath) == src { | |
// Skip the root of the src directory, the posts are in the subdirectories. | |
return nil | |
} | |
if strings.HasSuffix(currentPath, ".gmi") { | |
geminiFiles = append(geminiFiles, currentPath) | |
} | |
return nil | |
}) | |
if err != nil { | |
err = fmt.Errorf("failed to walk directory: %w", err) | |
return | |
} | |
posts = make([]Post, len(geminiFiles)) | |
for i, gf := range geminiFiles { | |
posts[i].FileName = gf | |
posts[i].Metadata, err = loadMetadata(path.Join(path.Dir(gf), "metadata.json")) | |
if err != nil { | |
err = fmt.Errorf("failed to load metadata %q: %w", gf, err) | |
return | |
} | |
if posts[i].Metadata.URL != trimRootAndFileName(src, posts[i].FileName) { | |
fmt.Printf("WARN: mismatched URL and filename: %q -> %q\n", posts[i].Metadata.URL, trimRootAndFileName(src, posts[i].FileName)) | |
} | |
err = nil | |
} | |
sort.Slice(posts, func(i, j int) bool { | |
return posts[i].Metadata.Date.After(posts[j].Metadata.Date) | |
}) | |
return | |
} | |
func trimRootAndFileName(prefix, fn string) string { | |
dir, _ := path.Split(fn) | |
return strings.TrimPrefix(strings.TrimSuffix(dir, "/"), prefix) | |
} | |
func loadMetadata(fileName string) (m Metadata, err error) { | |
f, err := os.Open(fileName) | |
if err != nil { | |
return | |
} | |
dec := json.NewDecoder(f) | |
err = dec.Decode(&m) | |
if err == nil { | |
m.URL = strings.TrimSuffix(m.URL, "/") | |
} | |
return | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment