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 }