summaryrefslogtreecommitdiffstats
path: root/internal/cms/menu.go
diff options
context:
space:
mode:
authorGab Virebent <gabriel1@virebent.art>2026-07-07 16:24:28 +0200
committerGab Virebent <gabriel1@virebent.art>2026-07-07 16:24:28 +0200
commit37f156c30e2a2b01c840d39f0dd2bdbf811732d6 (patch)
treee2ce84dc7f88ce0eb312899448f91b7bb93ca8a3 /internal/cms/menu.go
downloadgemcms-main.tar.gz
gemcms-main.tar.xz
gemcms-main.zip
Initial commitHEADmain
Diffstat (limited to 'internal/cms/menu.go')
-rw-r--r--internal/cms/menu.go193
1 files changed, 193 insertions, 0 deletions
diff --git a/internal/cms/menu.go b/internal/cms/menu.go
new file mode 100644
index 0000000..f430525
--- /dev/null
+++ b/internal/cms/menu.go
@@ -0,0 +1,193 @@
+package cms
+
+import (
+ "fmt"
+ "io/fs"
+ "os"
+ "path/filepath"
+ "strings"
+)
+
+func CreateMenu(root, name string) (string, error) {
+ name = slugify(name)
+ if name == "" || name == "untitled" {
+ return "", fmt.Errorf("menu name is required")
+ }
+ path := filepath.Join(root, "site/menus", name+".toml")
+ data := "name = " + formatString(name) + "\n\n"
+ return path, writeNewFile(path, []byte(data))
+}
+
+func LoadMenus(root string) (map[string]Menu, error) {
+ dir := filepath.Join(root, "site/menus")
+ menus := map[string]Menu{}
+ if _, err := os.Stat(dir); err != nil {
+ if os.IsNotExist(err) {
+ return menus, nil
+ }
+ return nil, err
+ }
+ err := filepath.WalkDir(dir, func(path string, entry fs.DirEntry, err error) error {
+ if err != nil {
+ return err
+ }
+ if entry.IsDir() || filepath.Ext(path) != ".toml" {
+ return nil
+ }
+ menu, err := loadMenu(path)
+ if err != nil {
+ return err
+ }
+ menus[menu.Name] = menu
+ return nil
+ })
+ return menus, err
+}
+
+func AddMenuItem(root string, opts MenuItemOptions) error {
+ name := slugify(opts.Menu)
+ if name == "" || name == "untitled" {
+ return fmt.Errorf("menu name is required")
+ }
+ path := filepath.Join(root, "site/menus", name+".toml")
+ menu, err := loadMenu(path)
+ if err != nil {
+ if os.IsNotExist(err) {
+ if _, createErr := CreateMenu(root, name); createErr != nil {
+ return createErr
+ }
+ menu = Menu{Name: name}
+ } else {
+ return err
+ }
+ }
+ item, err := resolveMenuItem(opts)
+ if err != nil {
+ return err
+ }
+ menu.Items = append(menu.Items, item)
+ return saveMenu(path, menu)
+}
+
+func loadMenu(path string) (Menu, error) {
+ data, err := os.ReadFile(path)
+ if err != nil {
+ return Menu{}, err
+ }
+ menu := Menu{Name: strings.TrimSuffix(filepath.Base(path), filepath.Ext(path))}
+ var current *MenuItem
+
+ for lineNo, raw := range strings.Split(string(data), "\n") {
+ line := strings.TrimSpace(raw)
+ if line == "" || strings.HasPrefix(line, "#") {
+ continue
+ }
+ if line == "[[items]]" {
+ menu.Items = append(menu.Items, MenuItem{})
+ current = &menu.Items[len(menu.Items)-1]
+ continue
+ }
+ key, rawValue, ok := strings.Cut(line, "=")
+ if !ok {
+ return Menu{}, fmt.Errorf("%s:%d: expected key = value", path, lineNo+1)
+ }
+ value, err := parseScalar(strings.TrimSpace(rawValue))
+ if err != nil {
+ return Menu{}, fmt.Errorf("%s:%d: %w", path, lineNo+1, err)
+ }
+ key = strings.TrimSpace(key)
+ if current == nil {
+ if key == "name" {
+ menu.Name = value
+ }
+ continue
+ }
+ switch key {
+ case "label":
+ current.Label = value
+ case "url":
+ current.URL = value
+ }
+ }
+ if menu.Name == "" {
+ menu.Name = strings.TrimSuffix(filepath.Base(path), filepath.Ext(path))
+ }
+ return menu, nil
+}
+
+func saveMenu(path string, menu Menu) error {
+ var b strings.Builder
+ b.WriteString("name = " + formatString(menu.Name) + "\n")
+ for _, item := range menu.Items {
+ b.WriteString("\n[[items]]\n")
+ b.WriteString("label = " + formatString(item.Label) + "\n")
+ b.WriteString("url = " + formatString(item.URL) + "\n")
+ }
+ return writeFile(path, []byte(b.String()))
+}
+
+func resolveMenuItem(opts MenuItemOptions) (MenuItem, error) {
+ label := opts.Label
+ url := opts.URL
+ target := slugify(opts.Target)
+
+ switch opts.Kind {
+ case "home":
+ if label == "" {
+ label = "Home"
+ }
+ if url == "" {
+ url = "/"
+ }
+ case "page":
+ if target == "" || target == "untitled" {
+ return MenuItem{}, fmt.Errorf("page menu item requires a target")
+ }
+ if label == "" {
+ label = titleize(target)
+ }
+ if url == "" {
+ if target == "index" {
+ url = "/"
+ } else {
+ url = "/" + target + ".gmi"
+ }
+ }
+ case "section":
+ if target == "" || target == "untitled" {
+ return MenuItem{}, fmt.Errorf("section menu item requires a target")
+ }
+ if label == "" {
+ label = titleize(target)
+ }
+ if url == "" {
+ url = "/" + target + "/"
+ }
+ case "article":
+ if target == "" || target == "untitled" {
+ return MenuItem{}, fmt.Errorf("article menu item requires a target")
+ }
+ if label == "" {
+ label = titleize(target)
+ }
+ if url == "" {
+ url = "/articles/" + target + ".gmi"
+ }
+ case "custom":
+ if label == "" {
+ label = titleize(target)
+ }
+ if url == "" {
+ return MenuItem{}, fmt.Errorf("custom menu item requires --url")
+ }
+ default:
+ if label == "" {
+ label = titleize(opts.Kind)
+ }
+ if url == "" {
+ url = "/" + slugify(opts.Kind) + ".gmi"
+ }
+ }
+
+ return MenuItem{Label: label, URL: url}, nil
+}