commit fc40cf5
Artur Manuel
·
2026-07-31 19:02:02 +0000 UTC
parent 3f1b486
???????????????????????????
+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+}
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+}