commit fc40cf5

Artur Manuel  ·  2026-07-31 19:02:02 +0000 UTC
parent 3f1b486
???????????????????????????
8 files changed,  +308, -3
M go.mod
M go.sum
+2, -3
 1@@ -1,9 +1,8 @@
 2 **
 3 !/cmd
 4 !/cmd/**
 5-!/pkg
 6-!/pkg/ssg
 7-!/pkg/ssg/**
 8+!/ssg
 9+!/ssg/**
10 !/.gitignore
11 !/.editorconfig
12 !/Makefile
M go.mod
+1, -0
1@@ -4,6 +4,7 @@ go 1.26.2
2 
3 require (
4 	github.com/BurntSushi/toml v1.6.0 // indirect
5+	github.com/alecthomas/kong v1.16.0 // indirect
6 	github.com/urfave/cli/v3 v3.8.0 // indirect
7 	github.com/yuin/goldmark v1.8.2 // indirect
8 	github.com/yuin/goldmark-meta v1.1.0 // indirect
M go.sum
+2, -0
1@@ -1,5 +1,7 @@
2 github.com/BurntSushi/toml v1.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk=
3 github.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
4+github.com/alecthomas/kong v1.16.0 h1:g92/kUxBcdcTPOM79yE63viJgtcp5dNyrB3/O2cjYT4=
5+github.com/alecthomas/kong v1.16.0/go.mod h1:wrlbXem1CWqUV5Vbmss5ISYhsVPkBb1Yo7YKJghju2I=
6 github.com/urfave/cli/v3 v3.8.0 h1:XqKPrm0q4P0q5JpoclYoCAv0/MIvH/jZ2umzuf8pNTI=
7 github.com/urfave/cli/v3 v3.8.0/go.mod h1:ysVLtOEmg2tOy6PknnYVhDoouyC/6N42TMeoMzskhso=
8 github.com/yuin/goldmark v1.8.2 h1:kEGpgqJXdgbkhcOgBxkC0X0PmoPG1ZyoZ117rDVp4zE=
+26, -0
 1@@ -0,0 +1,26 @@
 2+package ssg
 3+
 4+import (
 5+	"os"
 6+
 7+	"github.com/BurntSushi/toml"
 8+)
 9+
10+type Config struct {
11+	BaseURL     string         `toml:"base_url"`
12+	Title       string         `toml:"title"`
13+	Description string         `toml:"description"`
14+	Extra       map[string]any `toml:"extra"`
15+	SidebarOrder []string      `toml:"sidebar_order"`
16+}
17+
18+func LoadConfigFile(fn string, config *Config) error {
19+	out, err := os.ReadFile(fn)
20+	if err != nil {
21+		return err
22+	}
23+	if err := toml.Unmarshal(out, config); err != nil {
24+		return err
25+	}
26+	return nil
27+}
+103, -0
  1@@ -0,0 +1,103 @@
  2+package ssg
  3+
  4+import (
  5+	"bytes"
  6+	"io"
  7+	"io/fs"
  8+	"log/slog"
  9+	"os"
 10+	"path/filepath"
 11+	"strings"
 12+
 13+	"github.com/yuin/goldmark"
 14+	"github.com/yuin/goldmark-meta"
 15+	"github.com/yuin/goldmark/parser"
 16+)
 17+
 18+type PageError int
 19+
 20+const (
 21+	_ PageError = iota
 22+	NonExistentTemplate
 23+)
 24+
 25+func (e PageError) Error() string {
 26+	switch e {
 27+	case NonExistentTemplate:
 28+		return "given template does not exist"
 29+	}
 30+	panic("this should not be reached")
 31+}
 32+
 33+func GetPages(dir string) (map[string]string, error) {
 34+	res := make(map[string]string, 0)
 35+	err := filepath.WalkDir(dir, func(path string, d fs.DirEntry, err error) error {
 36+		if err != nil {
 37+			slog.Error("got err while reading path", "path", path, "err", err)
 38+			return filepath.SkipDir
 39+		}
 40+		if !d.IsDir() && strings.HasSuffix(path, ".md") {
 41+			content, err := os.ReadFile(path)
 42+			if err != nil {
 43+				slog.Error("got err while reading file", "file", path, "err", err)
 44+				return filepath.SkipDir
 45+			}
 46+			res[path] = string(content)
 47+		}
 48+		return nil
 49+	})
 50+	if err != nil {
 51+		return res, err
 52+	}
 53+	return res, nil
 54+}
 55+
 56+func BuildPage(w io.Writer, name string, path string, page string, markdown goldmark.Markdown, templates Templates, config Config, baseUrl string, sidebar []SidebarItem) error {
 57+	var buff bytes.Buffer
 58+	ctx := parser.NewContext()
 59+	if err := markdown.Convert([]byte(page), &buff, parser.WithContext(ctx)); err != nil {
 60+		return err
 61+	}
 62+	frontmatter := meta.Get(ctx)
 63+	var templateName string
 64+	switch v := frontmatter["template"]; v {
 65+	case nil:
 66+		if name == "index" {
 67+			templateName = "index"
 68+		} else {
 69+			templateName = "page"
 70+		}
 71+	default:
 72+		templateName = v.(string)
 73+	}
 74+	if baseUrl == "" {
 75+		baseUrl = config.BaseURL
 76+	}
 77+	var title string
 78+	switch v := frontmatter["title"]; v {
 79+	case nil:
 80+		title = config.Title
 81+	default:
 82+		title = v.(string)
 83+	}
 84+	var description string
 85+	switch v := frontmatter["description"]; v {
 86+	case nil:
 87+		description = config.Description
 88+	default:
 89+		description = v.(string)
 90+	}
 91+	templateData := TemplateData{
 92+		Title:       title,
 93+		Description: description,
 94+		BaseURL:     baseUrl,
 95+		Params:      frontmatter,
 96+		Config:      config,
 97+		Content:     buff.String(),
 98+		Sidebar:     sidebar,
 99+	}
100+	if err := BuildTemplate(w, name, templates[templateName], templateData); err != nil {
101+		return err
102+	}
103+	return nil
104+}
+32, -0
 1@@ -0,0 +1,32 @@
 2+package ssg
 3+
 4+import (
 5+	"os"
 6+	"strings"
 7+)
 8+
 9+func GetSidebar(pagesDir string, order []string) ([]SidebarItem, error) {
10+	var nav []SidebarItem
11+	for _, item := range order {
12+		nav = append(nav, SidebarItem{
13+			Name: item,
14+			Path: item + ".html",
15+		})
16+	}
17+	entries, err := os.ReadDir(pagesDir)
18+	if err != nil {
19+		return nav, err
20+	}
21+	for _, entry := range entries {
22+		stem, ok := strings.CutSuffix(entry.Name(), ".md")
23+		if !ok {
24+			continue
25+		}
26+		nav = append(nav, SidebarItem{
27+			Name: stem,
28+			Path: stem + ".html",
29+		})
30+	}
31+	nav = RemoveDuplicateItems(nav)
32+	return nav, nil
33+}
+73, -0
 1@@ -0,0 +1,73 @@
 2+package ssg
 3+
 4+import (
 5+	"io"
 6+	"os"
 7+	"path/filepath"
 8+	"strings"
 9+	"text/template"
10+)
11+
12+type TemplateData struct {
13+	Title       string
14+	Description string
15+	BaseURL     string
16+	Params      map[string]any
17+	Config      Config
18+	Content     string
19+	Sidebar     []SidebarItem
20+}
21+
22+type SidebarItem struct {
23+	Name string
24+	Path string
25+}
26+
27+type Templates map[string]string
28+
29+func DefaultTemplateData() TemplateData {
30+	return TemplateData{
31+		Title:       "",
32+		Description: "",
33+		BaseURL:     "",
34+		Params:      map[string]any{},
35+		Config:      Config{},
36+		Content:     "",
37+	}
38+}
39+
40+func BuildTemplate(w io.Writer, name string, t string, data TemplateData) error {
41+	master, err := template.New(name).Parse(t)
42+	if err != nil {
43+		return err
44+	}
45+	_, err = master.New("content").Parse(data.Content)
46+	if err != nil {
47+		return err
48+	}
49+	if err := master.ExecuteTemplate(w, name, data); err != nil {
50+		return err
51+	}
52+	return nil
53+}
54+
55+func GetTemplates(dir string) (Templates, error) {
56+	res := make(map[string]string, 0)
57+	entries, err := os.ReadDir(dir)
58+	if err != nil {
59+		return res, err
60+	}
61+	for _, entry := range entries {
62+		entryName := entry.Name()
63+		if !strings.HasSuffix(entryName, ".html") {
64+			continue
65+		}
66+		name := strings.TrimSuffix(filepath.Base(entryName), ".html")
67+		content, err := os.ReadFile(filepath.Join(dir, entryName))
68+		if err != nil {
69+			return res, err
70+		}
71+		res[name] = string(content)
72+	}
73+	return res, nil
74+}
+69, -0
 1@@ -0,0 +1,69 @@
 2+package ssg
 3+
 4+import (
 5+	"io"
 6+	"os"
 7+	"path/filepath"
 8+)
 9+
10+func CopyDir(source string, destination string) error {
11+	srcInfo, err := os.Stat(source)
12+	if err != nil {
13+		return err
14+	}
15+	if err := os.MkdirAll(destination, srcInfo.Mode()); err != nil {
16+		return err
17+	}
18+	entries, err := os.ReadDir(source)
19+	if err != nil {
20+		return err
21+	}
22+	for _, entry := range entries {
23+		entrySrc := filepath.Join(source, entry.Name())
24+		entryDest := filepath.Join(destination, entry.Name())
25+		if entry.IsDir() {
26+			if err := CopyDir(entrySrc, entryDest); err != nil {
27+				return err
28+			}
29+		} else {
30+			if err := CopyFile(entrySrc, entryDest); err != nil {
31+				return err
32+			}
33+		}
34+	}
35+	return nil
36+}
37+
38+func CopyFile(srcPath string, destPath string) error {
39+	info, err := os.Stat(srcPath)
40+	if err != nil {
41+		return err
42+	}
43+	src, err := os.Open(srcPath)
44+	if err != nil {
45+		return err
46+	}
47+	defer src.Close()
48+	dest, err := os.OpenFile(destPath, os.O_WRONLY|os.O_CREATE, info.Mode())
49+	if err != nil {
50+		return err
51+	}
52+	defer dest.Close()
53+	_, err = io.Copy(dest, src)
54+	if err != nil {
55+		return err
56+	}
57+	return nil
58+}
59+
60+func RemoveDuplicateItems[T comparable](items []T) []T {
61+	result := make([]T, 0)
62+	seen := make(map[T]bool, 0)
63+	for _, item := range items {
64+		if !seen[item] {
65+			result = append(result, item)
66+			seen[item] = true
67+		}
68+	}
69+	return result
70+}