package cms import ( "fmt" "io/fs" "os" "path/filepath" "sort" "strings" ) func LoadDocuments(root string) ([]Document, error) { contentRoot := filepath.Join(root, "content") var docs []Document if _, err := os.Stat(contentRoot); err != nil { return nil, err } err := filepath.WalkDir(contentRoot, func(path string, entry fs.DirEntry, err error) error { if err != nil { return err } if entry.IsDir() || filepath.Ext(path) != ".gmi" { return nil } data, err := os.ReadFile(path) if err != nil { return err } doc, err := ParseDocument(path, string(data)) if err != nil { return err } docs = append(docs, doc) return nil }) if err != nil { return nil, err } sort.Slice(docs, func(i, j int) bool { if docs[i].Type != docs[j].Type { return docs[i].Type < docs[j].Type } if docs[i].Date != docs[j].Date { return docs[i].Date > docs[j].Date } return docs[i].Slug < docs[j].Slug }) return docs, nil } func ParseDocument(path, data string) (Document, error) { meta, body, hasMeta := splitFrontmatter(data) doc := Document{ SourcePath: path, Body: strings.TrimLeft(body, "\n"), } if hasMeta { table, err := parseMetadata(meta) if err != nil { return Document{}, fmt.Errorf("%s: parse frontmatter: %w", path, err) } doc.Type = table.values["type"] doc.Slug = table.values["slug"] doc.Title = table.values["title"] doc.Date = table.values["date"] doc.Section = table.values["section"] doc.Tags = table.lists["tags"] doc.Draft = table.values["draft"] == "true" } if doc.Type == "" { doc.Type = inferType(path) } if doc.Slug == "" { doc.Slug = inferSlug(path) } if doc.Title == "" { doc.Title = inferTitle(doc.Body, doc.Slug) } if doc.Type == "article" { if doc.Section == "" { doc.Section = "articles" } if doc.Date == "" { doc.Date = inferDate(path) } } return doc, nil } func writeDocument(path string, doc Document) error { var b strings.Builder b.WriteString("+++\n") b.WriteString("title = " + formatString(doc.Title) + "\n") b.WriteString("type = " + formatString(doc.Type) + "\n") b.WriteString("slug = " + formatString(doc.Slug) + "\n") if doc.Date != "" { b.WriteString("date = " + formatString(doc.Date) + "\n") } if doc.Section != "" { b.WriteString("section = " + formatString(doc.Section) + "\n") } if len(doc.Tags) > 0 { b.WriteString("tags = " + formatStringList(doc.Tags) + "\n") } if doc.Draft { b.WriteString("draft = true\n") } b.WriteString("+++\n\n") b.WriteString(strings.TrimLeft(doc.Body, "\n")) return writeNewFile(path, []byte(b.String())) } func splitFrontmatter(data string) (string, string, bool) { data = strings.ReplaceAll(data, "\r\n", "\n") delimiter := "" switch { case strings.HasPrefix(data, "+++\n"): delimiter = "+++" case strings.HasPrefix(data, "---\n"): delimiter = "---" default: return "", data, false } rest := strings.TrimPrefix(data, delimiter+"\n") idx := strings.Index(rest, "\n"+delimiter+"\n") if idx < 0 { return "", data, false } meta := rest[:idx] body := rest[idx+len("\n"+delimiter+"\n"):] return meta, body, true } func inferType(path string) string { clean := filepath.ToSlash(path) if strings.Contains(clean, "/articles/") { return "article" } return "page" } func inferSlug(path string) string { name := strings.TrimSuffix(filepath.Base(path), filepath.Ext(path)) if len(name) > len("2006-01-02-") && name[4] == '-' && name[7] == '-' && name[10] == '-' { name = name[11:] } return slugify(name) } func inferDate(path string) string { name := filepath.Base(path) if len(name) >= len("2006-01-02") && name[4] == '-' && name[7] == '-' { return name[:10] } return "" } func inferTitle(body, slug string) string { for _, line := range strings.Split(body, "\n") { line = strings.TrimSpace(line) if strings.HasPrefix(line, "# ") { return strings.TrimSpace(strings.TrimPrefix(line, "# ")) } } return titleize(slug) }