This repository has been archived on 2021-07-23. You can view files and clone it, but cannot push or open issues or pull requests.
sitegenerator/main.go

380 lines
9.4 KiB
Go

// A website generator tool
package main
import (
"encoding/json"
"fmt"
"io"
"io/ioutil"
"os"
"path/filepath"
"strings"
"tildegit.org/nihilazo/go-gemtext"
"github.com/gorilla/feeds"
t "text/template"
)
// TODO replace these hardcoded paths with config
var root string = "/mnt/term/home/nico/website/wiki"
var geminiPrefix string = "gemini://breadpunk.club/~bagel"
var htmlPrefix string = "https://itwont.work"
var geminiOutputDir string = filepath.Join(root, "public_gemini")
var templateDir string = filepath.Join(root, "templates")
var htmlOutputDir string = filepath.Join(root, "public_html")
var inputDir string = filepath.Join(root, "content")
var tagFile string = filepath.Join(root, "tags.json")
type filePathInfo struct {
Path string
New bool // True if the file is new
}
type pageInfo struct {
Path string
Title string
}
type templateTag struct {
Name string
Pages []pageInfo
} // Only used for tagpages
const (
GEMINI = iota
HTML
)
// RemoveIndex is a reslicing function.
func RemoveIndex(s []pageInfo, index int) []pageInfo {
return append(s[:index], s[index+1:]...)
}
// wraps the string in a basic standalone HTML document.
func htmlWrap(d *string) string {
header := `<!DOCTYPE html><head><link rel=stylesheet href="https://itwont.work/style.css"><title>Nico's site</title></head><body><article>`
footer := `</article></body>`
return header + *d + footer
}
var tagData map[string][]pageInfo = make(map[string][]pageInfo)
var files []filePathInfo // Files to process (those that are new or have been edited)
func copy(src, dst string) (int64, error) {
sourceFileStat, err := os.Stat(src)
if err != nil {
return 0, err
}
if !sourceFileStat.Mode().IsRegular() {
return 0, fmt.Errorf("%s is not a regular file", src)
}
source, err := os.Open(src)
if err != nil {
return 0, err
}
defer source.Close()
destination, err := os.Create(dst)
if err != nil {
return 0, err
}
defer destination.Close()
nBytes, err := io.Copy(destination, source)
return nBytes, err
}
// isTagged returns true if the page is a tagged gemtext page, false otherwise
func isTagged(page *gemtext.GemtextPage) bool {
p := *page
if len(p) > 2 {
if p[0].Type == gemtext.HEADING && p[1].Type == gemtext.TEXT {
if strings.HasPrefix(p[1].Text, "Tags:") {
return true
}
}
}
return false
}
// addPrefix adds the prefix "pref" to a path if it is absolute
// this is to allow protocol-independent absolute links.
func addPrefix(pref string, link string) string {
if filepath.IsAbs(link) {
return pref + link
} else {
return link
}
}
// processLink takes a LINK GemtextObject and returns it processed for either gemini or HTML output
func processLink(o gemtext.GemtextObject, mode int) gemtext.GemtextObject {
if mode == GEMINI {
o.Path = addPrefix(geminiPrefix, o.Path)
} else if mode == HTML {
o.Path = addPrefix(htmlPrefix, strings.Replace(o.Path, ".gmi", ".html", -1))
}
return o
}
func processLinkString(s string, mode int) (string, error) {
o, err := filepath.Rel(inputDir, s)
if err != nil {
return "", err
}
if mode == GEMINI {
o = geminiPrefix + "/" + o
} else if mode == HTML {
o = htmlPrefix + "/" + strings.Replace(o, ".gmi", ".html", -1)
}
return o, nil
}
// processPage finds a page, gets the tags, and writes it out to HTML and gemini output directories.
// TODO BUG: somehow tagData ends up empty.
func processPage(f filePathInfo) error {
// Delete any existing tag references to this file.
if filepath.Ext(f.Path) == ".gmi" {
for i, d := range tagData { // For each tag in tagdata
if len(d) != 0 {
for j, _ := range d { // for each file in the tag
if d[j].Path == f.Path {
tagData[i] = RemoveIndex(d, j)
}
}
}
}
// Open the file and parse the gemtext
data, err := ioutil.ReadFile(f.Path)
if err != nil {
return err
}
parse, err := gemtext.ParsePage(string(data))
if err != nil {
return err
}
if isTagged(&parse) {
title := parse[0].Text
data := pageInfo{Title: title, Path: f.Path}
tags := strings.Split(parse[1].Text[5:], ",") // Remove the Tags: prefix and split
for _, tag := range tags {
tagData[tag] = append(tagData[tag], data) // For each tag, append the data to it
}
}
var gemlinks gemtext.GemtextPage // Page with prefixed links for gemini
var htmllinks gemtext.GemtextPage // Page with prefixed links for html
for _, line := range parse {
if line.Type == gemtext.LINK {
gemlinks = append(gemlinks, processLink(line, GEMINI))
htmllinks = append(htmllinks, processLink(line, HTML))
} else {
gemlinks = append(gemlinks, line)
htmllinks = append(htmllinks, line)
}
}
geminiRender, err := gemtext.RenderGemtext(&gemlinks)
if err != nil {
return err
}
rel, err := filepath.Rel(inputDir, f.Path)
if err != nil {
return err
}
output := filepath.Join(geminiOutputDir, rel)
err = ioutil.WriteFile(output, []byte(geminiRender), 0644)
if err != nil {
return err
}
htmlRender, err := gemtext.RenderHTML(&htmllinks)
if err != nil {
return err
}
output = strings.Replace(filepath.Join(htmlOutputDir, rel), ".gmi", ".html", -1)
err = ioutil.WriteFile(output, []byte(htmlWrap(&htmlRender)), 0644)
if err != nil {
return err
}
} else { // Non-gemtext files
rel, err := filepath.Rel(inputDir, f.Path)
if err != nil {
return err
}
copy(f.Path, filepath.Join(htmlOutputDir, rel))
copy(f.Path, filepath.Join(geminiOutputDir, rel))
}
return nil
}
// walkInputDir is called when walking the input directory, finds what files are new or have been edited, adds them to files
func walkInputDir(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
if !info.IsDir() {
rel, err := filepath.Rel(inputDir, path)
if err != nil {
return err
}
outPath := filepath.Join(geminiOutputDir, rel)
outFileInfo, err := os.Stat(outPath)
if err != nil {
if os.IsNotExist(err) {
files = append(files, filePathInfo{Path: path, New: true})
} else {
return err
}
} else {
if info.ModTime().After(outFileInfo.ModTime()) {
files = append(files, filePathInfo{Path: path})
}
}
}
return nil
}
// writeTagPage writes a tag page.
func writeTagPage(tag string, pages *[]pageInfo, t *t.Template, basePath string, linkType int) error {
var outPath string
if linkType == HTML {
outPath = filepath.Join(basePath, tag+".html")
} else {
outPath = filepath.Join(basePath, tag+".gmi")
}
f, err := os.Create(filepath.Join(outPath))
defer f.Close()
if err != nil {
return err
}
np := make([]pageInfo, len(*pages))
for i, p := range *pages {
np[i].Path, err = processLinkString(p.Path, linkType)
np[i].Title = p.Title
if err != nil {
return err
}
}
err = t.Execute(f, templateTag{Name: tag, Pages: np})
if err != nil {
return err
}
f.Sync()
return nil
}
func main() {
loadingJson := true
if _, err := os.Stat(tagFile); err != nil {
if os.IsNotExist(err) {
loadingJson = false
fmt.Println("Not loading tags file, it doesn't exist yet")
} else {
panic(err)
}
}
tagFileData, err := ioutil.ReadFile(tagFile)
if loadingJson {
err = json.Unmarshal([]byte(tagFileData), &tagData)
if err != nil {
panic(err)
}
}
htmlTagPageTemplateFile, err := ioutil.ReadFile(filepath.Join(templateDir, "tag.html"))
if err != nil {
panic(err)
}
htmlTagPageTemplate, err := t.New("htmlTagPage").Parse(string(htmlTagPageTemplateFile))
if err != nil {
panic(err)
}
geminiTagPageTemplateFile, err := ioutil.ReadFile(filepath.Join(templateDir, "tag.gmi"))
if err != nil {
panic(err)
}
geminiTagPageTemplate, err := t.New("geminiTagPage").Parse(string(geminiTagPageTemplateFile))
if err != nil {
panic(err)
}
err = filepath.Walk(inputDir, walkInputDir) // walks the tree, creates the files slice and updates tagData
if err != nil {
panic(err)
}
for _, file := range files {
err := processPage(file) // Process all the updated files.
if err != nil {
panic(err)
}
}
if err := os.Mkdir(filepath.Join(htmlOutputDir, "_tags"), 0664); err != nil {
if !os.IsExist(err) {
panic(err)
}
}
if err := os.Mkdir(filepath.Join(geminiOutputDir, "_tags"), 0664); err != nil {
if !os.IsExist(err) {
panic(err)
}
}
// Write out index page
templateFile, err := ioutil.ReadFile(filepath.Join(templateDir, "index.gmi"))
if err != nil {
panic(err)
}
indexTemplate, err := t.New("gemtextIndex").Parse(string(templateFile))
if err != nil {
panic(err)
}
f, err := os.Create(filepath.Join(geminiOutputDir, "index.gmi"))
defer f.Close()
if err != nil {
panic(err)
}
err = indexTemplate.Execute(f, tagData)
if err != nil {
panic(err)
}
f.Sync()
// Write out HTML index page
templateFile, err = ioutil.ReadFile(filepath.Join(templateDir, "index.html"))
if err != nil {
panic(err)
}
indexTemplate, err = t.New("htmlIndex").Parse(string(templateFile))
if err != nil {
panic(err)
}
hf, err := os.Create(filepath.Join(htmlOutputDir, "index.html"))
defer hf.Close()
if err != nil {
panic(err)
}
err = indexTemplate.Execute(hf, tagData)
if err != nil {
panic(err)
}
hf.Sync()
// Write tag pages
for tag, pages := range tagData {
err = writeTagPage(tag, &pages, htmlTagPageTemplate, filepath.Join(htmlOutputDir, "/_tags/"), HTML)
if err != nil {
panic(err)
}
err = writeTagPage(tag, &pages, geminiTagPageTemplate, filepath.Join(geminiOutputDir, "/_tags/"), GEMINI)
if err != nil {
panic(err)
}
}
json, err := json.Marshal(tagData)
err = ioutil.WriteFile(tagFile, json, 0644)
if err != nil {
panic(err)
}
}