Add the CLI, site, and sample content so the project can run locally. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
130 lines
2.9 KiB
Go
130 lines
2.9 KiB
Go
package cli
|
|
|
|
import (
|
|
"bytes"
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
|
|
"gopkg.in/yaml.v3"
|
|
)
|
|
|
|
func loadPosts(root string) ([]postFile, error) {
|
|
postsDir := filepath.Join(root, defaultPostsDir)
|
|
entries, err := os.ReadDir(postsDir)
|
|
if err != nil {
|
|
if errors.Is(err, os.ErrNotExist) {
|
|
return nil, nil
|
|
}
|
|
return nil, err
|
|
}
|
|
|
|
var posts []postFile
|
|
for _, entry := range entries {
|
|
if entry.IsDir() || !strings.HasSuffix(entry.Name(), ".md") {
|
|
continue
|
|
}
|
|
|
|
post, err := readPostFile(filepath.Join(postsDir, entry.Name()))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
posts = append(posts, post)
|
|
}
|
|
return posts, nil
|
|
}
|
|
|
|
func loadPostBySlug(root string, slug string) (postFile, error) {
|
|
cleanSlug := sanitizeSlug(slug)
|
|
if cleanSlug == "" {
|
|
return postFile{}, errors.New("missing slug")
|
|
}
|
|
|
|
posts, err := loadPosts(root)
|
|
if err != nil {
|
|
return postFile{}, err
|
|
}
|
|
for _, post := range posts {
|
|
fileSlug := strings.TrimSuffix(filepath.Base(post.Path), ".md")
|
|
if post.Frontmatter.Slug == cleanSlug || fileSlug == cleanSlug {
|
|
return post, nil
|
|
}
|
|
}
|
|
return postFile{}, fmt.Errorf("post not found: %s", cleanSlug)
|
|
}
|
|
|
|
func readPostFile(path string) (postFile, error) {
|
|
data, err := os.ReadFile(path)
|
|
if err != nil {
|
|
return postFile{}, err
|
|
}
|
|
|
|
frontmatter, body, err := splitFrontmatter(data)
|
|
if err != nil {
|
|
return postFile{}, fmt.Errorf("%s: %w", path, err)
|
|
}
|
|
|
|
var meta postFrontmatter
|
|
if err := yaml.Unmarshal(frontmatter, &meta); err != nil {
|
|
return postFile{}, fmt.Errorf("%s: %w", path, err)
|
|
}
|
|
if meta.Slug == "" {
|
|
meta.Slug = strings.TrimSuffix(filepath.Base(path), ".md")
|
|
}
|
|
if meta.Status == "" {
|
|
meta.Status = "draft"
|
|
}
|
|
|
|
return postFile{
|
|
Path: path,
|
|
Frontmatter: meta,
|
|
Body: strings.TrimPrefix(string(body), "\n"),
|
|
}, nil
|
|
}
|
|
|
|
func writePostFile(post postFile) error {
|
|
var frontmatter bytes.Buffer
|
|
encoder := yaml.NewEncoder(&frontmatter)
|
|
encoder.SetIndent(2)
|
|
if err := encoder.Encode(post.Frontmatter); err != nil {
|
|
return err
|
|
}
|
|
if err := encoder.Close(); err != nil {
|
|
return err
|
|
}
|
|
|
|
var output bytes.Buffer
|
|
output.WriteString("---\n")
|
|
output.Write(frontmatter.Bytes())
|
|
output.WriteString("---\n\n")
|
|
output.WriteString(strings.TrimLeft(post.Body, "\n"))
|
|
|
|
tmp := post.Path + ".tmp"
|
|
if err := os.WriteFile(tmp, output.Bytes(), 0o644); err != nil {
|
|
return err
|
|
}
|
|
return os.Rename(tmp, post.Path)
|
|
}
|
|
|
|
func splitFrontmatter(data []byte) ([]byte, []byte, error) {
|
|
if !bytes.HasPrefix(data, []byte("---\n")) {
|
|
return nil, nil, errors.New("missing frontmatter opening marker")
|
|
}
|
|
|
|
rest := data[len("---\n"):]
|
|
idx := bytes.Index(rest, []byte("\n---"))
|
|
if idx < 0 {
|
|
return nil, nil, errors.New("missing frontmatter closing marker")
|
|
}
|
|
|
|
frontmatter := rest[:idx]
|
|
body := rest[idx+len("\n---"):]
|
|
if bytes.HasPrefix(body, []byte("\r\n")) {
|
|
body = body[2:]
|
|
} else if bytes.HasPrefix(body, []byte("\n")) {
|
|
body = body[1:]
|
|
}
|
|
return frontmatter, body, nil
|
|
}
|