Repos / s4g / 8608d3b841
commit 8608d3b841bba717b2c4219aae2031cf208bcf87
Author: Nhân <hi@imnhan.com>
Date:   Mon Jul 10 18:41:10 2023 +0700

    simpler metadata format
    
    `Key: val` feels more natural to write.
    As a bonus, we no longer need the toml dependency.

diff --git a/feed.go b/feed.go
index 9dc9e1e..4cee63a 100644
--- a/feed.go
+++ b/feed.go
@@ -31,9 +31,9 @@ func generateFeed(site SiteMetadata, posts []Article, path string) []byte {
 		Updated: atom.Time(posts[0].PostedAt),
 		Entry:   entries,
 		Author: &atom.Person{
-			Name:  site.Author.Name,
-			URI:   site.Author.URI,
-			Email: site.Author.Email,
+			Name:  site.AuthorName,
+			URI:   site.AuthorURI,
+			Email: site.AuthorEmail,
 		},
 		Link: []atom.Link{{Rel: "self", Href: path}},
 	}
diff --git a/go.mod b/go.mod
index 432d81d..1435631 100644
--- a/go.mod
+++ b/go.mod
@@ -2,8 +2,6 @@ module go.imnhan.com/webmaker2000
 
 go 1.20
 
-require github.com/BurntSushi/toml v1.3.2
-
 require (
 	github.com/fsnotify/fsnotify v1.6.0
 	golang.org/x/tools v0.10.0
diff --git a/go.sum b/go.sum
index 27f5e0e..ec91993 100644
--- a/go.sum
+++ b/go.sum
@@ -1,5 +1,3 @@
-github.com/BurntSushi/toml v1.3.2 h1:o7IhLm0Msx3BaB+n3Ag7L8EVlByGnpq14C4YWiu/gL8=
-github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
 github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY=
 github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw=
 golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
diff --git a/main.go b/main.go
index 38472a3..0c0aeb8 100644
--- a/main.go
+++ b/main.go
@@ -14,7 +14,6 @@
 	"strings"
 	"time"
 
-	"github.com/BurntSushi/toml"
 	"go.imnhan.com/webmaker2000/djot"
 	"go.imnhan.com/webmaker2000/livereload"
 	"go.imnhan.com/webmaker2000/writablefs"
@@ -34,7 +33,7 @@ func main() {
 
 	if new != "" {
 		fmt.Println("Making new site at", new)
-		err := makeSite(new, newSiteMetadata())
+		err := makeSite(new, NewSiteMetadata())
 		if err != nil {
 			log.Fatal(err)
 		}
@@ -78,7 +77,7 @@ func main() {
 func regenerate(fsys writablefs.FS) {
 	defer timer("Took %s")()
 
-	site := readSiteMetadata(fsys)
+	site := ReadSiteMetadata(fsys)
 	articles := findArticles(fsys)
 
 	if len(articles) == 0 {
@@ -138,37 +137,6 @@ func regenerate(fsys writablefs.FS) {
 	WriteManifest(fsys, generatedFiles)
 }
 
-type SiteMetadata struct {
-	Address      string
-	Name         string
-	Tagline      string
-	HomePath     string
-	ShowFooter   bool
-	GenerateHome bool
-	Author       struct {
-		Name  string
-		URI   string
-		Email string
-	}
-}
-
-func newSiteMetadata() SiteMetadata {
-	return SiteMetadata{
-		HomePath:     "/",
-		ShowFooter:   true,
-		GenerateHome: true,
-	}
-}
-
-func readSiteMetadata(fsys writablefs.FS) SiteMetadata {
-	sm := newSiteMetadata()
-	_, err := toml.DecodeFS(fsys, SiteFileName, &sm)
-	if err != nil {
-		panic(err)
-	}
-	return sm
-}
-
 type Article struct {
 	Fs         writablefs.FS
 	Path       string
@@ -178,15 +146,6 @@ type Article struct {
 	ArticleMetadata
 }
 
-type ArticleMetadata struct {
-	Title      string
-	IsDraft    bool
-	PostedAt   time.Time
-	Templates  []string
-	ShowInFeed bool
-	ShowInNav  bool
-}
-
 func (a *Article) WebPath() string {
 	if a.webPath != "" {
 		return a.webPath
@@ -313,7 +272,7 @@ func findArticles(fsys writablefs.FS) (result []Article) {
 			ShowInFeed: true,
 			ShowInNav:  false,
 		}
-		_, err = toml.Decode(metaText, &meta)
+		err = UnmarshalMetadata([]byte(metaText), &meta)
 		if err != nil {
 			fmt.Printf("FIXME: Malformed article metadata in %s: %s\n", path, err)
 			return nil
diff --git a/makesite.go b/makesite.go
index 1a4ddec..789d243 100644
--- a/makesite.go
+++ b/makesite.go
@@ -8,8 +8,6 @@
 	"io/ioutil"
 	"os"
 	"path/filepath"
-
-	"github.com/BurntSushi/toml"
 )
 
 //go:embed theme
@@ -22,16 +20,9 @@ func makeSite(path string, meta SiteMetadata) error {
 		return fmt.Errorf("make site: %w", err)
 	}
 
-	// Create site metadata file
-	metaFilePath := filepath.Join(path, SiteFileName)
-	metaFile, err := os.Create(metaFilePath)
-	if err != nil {
-		return fmt.Errorf("create site metadata: %w", err)
-	}
-	defer metaFile.Close()
-
-	metaEncoder := toml.NewEncoder(metaFile)
-	err = metaEncoder.Encode(meta)
+	// Write site metadata file
+	data := MarshalMetadata(&meta)
+	err = ioutil.WriteFile(filepath.Join(path, SiteFileName), data, 0664)
 	if err != nil {
 		return fmt.Errorf("write site metadata: %w", err)
 	}
diff --git a/metadata.go b/metadata.go
new file mode 100644
index 0000000..50f138f
--- /dev/null
+++ b/metadata.go
@@ -0,0 +1,129 @@
+package main
+
+import (
+	"fmt"
+	"io/fs"
+	"reflect"
+	"strings"
+	"time"
+
+	"go.imnhan.com/webmaker2000/writablefs"
+)
+
+type SiteMetadata struct {
+	Address      string
+	Name         string
+	Tagline      string
+	HomePath     string
+	ShowFooter   bool
+	GenerateHome bool
+	AuthorName   string
+	AuthorURI    string
+	AuthorEmail  string
+}
+
+type ArticleMetadata struct {
+	Title      string
+	IsDraft    bool
+	PostedAt   time.Time
+	Templates  []string
+	ShowInFeed bool
+	ShowInNav  bool
+}
+
+func NewSiteMetadata() SiteMetadata {
+	return SiteMetadata{
+		HomePath:     "/",
+		ShowFooter:   true,
+		GenerateHome: true,
+	}
+}
+
+func ReadSiteMetadata(fsys writablefs.FS) SiteMetadata {
+	sm := NewSiteMetadata()
+
+	data, err := fs.ReadFile(fsys, SiteFileName)
+	if err != nil {
+		panic(err)
+	}
+
+	UnmarshalMetadata(data, &sm)
+	return sm
+}
+
+// Similar API to json.Unmarshal but supports neither struct tags nor nesting.
+func UnmarshalMetadata(data []byte, dest any) error {
+	m := metaTextToMap(data)
+
+	s := reflect.ValueOf(dest).Elem()
+	sType := s.Type()
+	for i := 0; i < s.NumField(); i++ {
+		f := s.Field(i)
+		val, ok := m[sType.Field(i).Name]
+		if ok {
+			switch f.Type().String() {
+			case "string":
+				s.Field(i).SetString(val)
+
+			case "bool":
+				if val != "true" && val != "false" {
+					return fmt.Errorf(
+						"invalid boolean: expected true/false, got %s", val,
+					)
+				}
+				s.Field(i).SetBool(val == "true")
+
+			case "time.Time":
+				tVal, err := time.ParseInLocation("2006-01-02", val, time.Local)
+				tVal = tVal.Local()
+				if err != nil {
+					return fmt.Errorf(
+						"invalid date: expected YYYY-MM-DD, got %s", val,
+					)
+				}
+				s.Field(i).Set(reflect.ValueOf(tVal))
+
+			default:
+				panic(fmt.Sprintf(
+					"unsupported metadata field type: %s",
+					f.Type().String(),
+				))
+			}
+		}
+	}
+	return nil
+}
+
+func MarshalMetadata(v any) []byte {
+	result := ""
+
+	s := reflect.ValueOf(v).Elem()
+	sType := s.Type()
+	for i := 0; i < s.NumField(); i++ {
+		f := s.Field(i)
+		key := sType.Field(i).Name
+		val := f.Interface()
+		result += fmt.Sprintf("%s: %v\n", key, val)
+	}
+
+	return []byte(result)
+}
+
+func metaTextToMap(s []byte) map[string]string {
+	result := make(map[string]string)
+	lines := strings.Split(strings.TrimSpace(string(s)), "\n")
+	for i, l := range lines {
+		if len(l) == 0 || l[0] == '#' {
+			continue
+		}
+		key, val, ok := strings.Cut(l, ":")
+		if !ok {
+			fmt.Printf("Metadata: invalid line %d: '%s'\n", i+1, l)
+			continue
+		}
+		// The trimming will also clean up the stray CR in
+		// Windows-style line breaks.
+		result[strings.TrimSpace(key)] = strings.TrimSpace(val)
+	}
+	return result
+}
diff --git a/theme/base.tmpl b/theme/base.tmpl
index 9ef2b9e..a1f0d72 100644
--- a/theme/base.tmpl
+++ b/theme/base.tmpl
@@ -15,7 +15,7 @@
 
 {{- if .Site.ShowFooter}}
 <footer>
-© {{if eq .StartYear .Now.Year}}{{.StartYear}}{{else}}{{.StartYear}}–{{.Now.Year}}{{end}} {{.Site.Author.Name}}<br>
+© {{if eq .StartYear .Now.Year}}{{.StartYear}}{{else}}{{.StartYear}}–{{.Now.Year}}{{end}} {{.Site.AuthorName}}<br>
 Made with <a href="https://github.com/nhanb/webmaker2000">WebMaker2000</a>
 </footer>
 {{- end}}
diff --git a/www/about/index.dj b/www/about/index.dj
index 685af49..a8fe1cf 100644
--- a/www/about/index.dj
+++ b/www/about/index.dj
@@ -1,7 +1,7 @@
 +++
-Title = "About"
-ShowInFeed = false
-ShowInNav = true
+Title: About
+ShowInFeed: false
+ShowInNav: true
 +++
 
 ## About this site
diff --git a/www/hello/index.dj b/www/hello/index.dj
index b5a2185..03c4e26 100644
--- a/www/hello/index.dj
+++ b/www/hello/index.dj
@@ -1,6 +1,6 @@
 +++
-Title = "Hello"
-PostedAt = 2022-01-02
+Title: Hello
+PostedAt: 2022-01-02
 +++
 
 Hello world.
diff --git a/www/mfws.dj b/www/mfws.dj
index 953384d..0b2dc40 100644
--- a/www/mfws.dj
+++ b/www/mfws.dj
@@ -1,6 +1,6 @@
 +++
-Title = "This is a motherfucking website."
-PostedAt = 2023-04-05
+Title: This is a motherfucking website.
+PostedAt: 2023-04-05
 +++
 
 And it's fucking perfect.
diff --git a/www/website.wbmkr2k b/www/website.wbmkr2k
index 947a0f1..9bd2671 100644
--- a/www/website.wbmkr2k
+++ b/www/website.wbmkr2k
@@ -1,11 +1,7 @@
-# vim: ft=toml
-# -*- mode: toml; -*-
+Address: https://coolzone.example.com
+Name: CoolZone
+Tagline: Cool people only.
 
-Address = "https://coolzone.example.com"
-Name = "CoolZone"
-Tagline = "Cool people only."
-
-[Author]
-Name = "Coolio McCool"
-URI = "https://author.example.com"
-Email = "coolio@example.com"
+AuthorName: Coolio McCool
+AuthorURI: https://author.example.com
+AuthorEmail: coolio@example.com