package content import ( "fmt" "regexp" "sort" "strings" "time" "gitblog/internal/github" "github.com/russross/blackfriday/v2" ) type Post struct { Title string Slug string Content string HTML string Date time.Time Categories []string Tags []string Excerpt string Path string LastUpdated time.Time } type Navigation struct { Title string URL string Order int } type Page struct { Title string Content string HTML string Path string } type Manager struct { githubClient *github.Client posts map[string]*Post pages map[string]*Page navigation []Navigation lastUpdate time.Time } func NewManager(githubClient *github.Client) *Manager { return &Manager{ githubClient: githubClient, posts: make(map[string]*Post), pages: make(map[string]*Page), navigation: []Navigation{}, } } func (m *Manager) LoadContent() error { if err := m.loadPosts(); err != nil { return fmt.Errorf("failed to load posts: %w", err) } if err := m.loadPages(); err != nil { return fmt.Errorf("failed to load pages: %w", err) } if err := m.loadNavigation(); err != nil { return fmt.Errorf("failed to load navigation: %w", err) } m.lastUpdate = time.Now() return nil } func (m *Manager) loadPosts() error { postsDir := "content/posts" contents, err := m.githubClient.ListDirectory(postsDir) if err != nil { return err } for _, content := range contents { if strings.HasSuffix(content.Path, ".md") { post, err := m.parsePost(content) if err != nil { // Log error but continue processing other posts fmt.Printf("Warning: failed to parse post %s: %v\n", content.Path, err) continue } if post.Slug != "" { m.posts[post.Slug] = post } } } return nil } func (m *Manager) loadPages() error { pagesDir := "content/pages" contents, err := m.githubClient.ListDirectory(pagesDir) if err != nil { return err } for _, content := range contents { if strings.HasSuffix(content.Path, ".md") { page, err := m.parsePage(content) if err != nil { continue } m.pages[strings.TrimSuffix(content.Path, ".md")] = page } } return nil } func (m *Manager) loadNavigation() error { navFile := "content/navigation.md" content, err := m.githubClient.GetFileContent(navFile) if err != nil { return err } lines := strings.Split(content.Content, "\n") for _, line := range lines { line = strings.TrimSpace(line) if line == "" || strings.HasPrefix(line, "#") { continue } parts := strings.SplitN(line, "|", 3) if len(parts) >= 2 { nav := Navigation{ Title: strings.TrimSpace(parts[0]), URL: strings.TrimSpace(parts[1]), } if len(parts) == 3 { fmt.Sscanf(strings.TrimSpace(parts[2]), "%d", &nav.Order) } m.navigation = append(m.navigation, nav) } } sort.Slice(m.navigation, func(i, j int) bool { return m.navigation[i].Order < m.navigation[j].Order }) return nil } func (m *Manager) parsePost(content *github.Content) (*Post, error) { lines := strings.Split(content.Content, "\n") var frontmatter map[string]interface{} var contentStart int for i, line := range lines { if strings.TrimSpace(line) == "---" { if i == 0 { frontmatter = m.parseFrontmatter(lines[1:]) contentStart = i + 1 break } } } contentLines := lines[contentStart:] for i, line := range contentLines { if strings.TrimSpace(line) == "---" { contentLines = contentLines[i+1:] break } } post := &Post{ Content: strings.Join(contentLines, "\n"), Path: content.Path, LastUpdated: content.LastUpdated, } if title, ok := frontmatter["title"].(string); ok { post.Title = title post.Slug = m.generateSlug(title) } if dateStr, ok := frontmatter["date"].(string); ok { if date, err := time.Parse("2006-01-02", dateStr); err == nil { post.Date = date } } if categories, ok := frontmatter["categories"].([]interface{}); ok { for _, cat := range categories { if catStr, ok := cat.(string); ok { post.Categories = append(post.Categories, catStr) } } } if tags, ok := frontmatter["tags"].([]interface{}); ok { for _, tag := range tags { if tagStr, ok := tag.(string); ok { post.Tags = append(post.Tags, tagStr) } } } post.HTML = string(blackfriday.Run([]byte(post.Content))) post.Excerpt = m.generateExcerpt(post.Content) return post, nil } func (m *Manager) parsePage(content *github.Content) (*Page, error) { lines := strings.Split(content.Content, "\n") var frontmatter map[string]interface{} var contentStart int for i, line := range lines { if strings.TrimSpace(line) == "---" { if i == 0 { frontmatter = m.parseFrontmatter(lines[1:]) contentStart = i + 1 break } } } contentLines := lines[contentStart:] for i, line := range contentLines { if strings.TrimSpace(line) == "---" { contentLines = contentLines[i+1:] break } } page := &Page{ Content: strings.Join(contentLines, "\n"), Path: content.Path, } if title, ok := frontmatter["title"].(string); ok { page.Title = title } page.HTML = string(blackfriday.Run([]byte(page.Content))) return page, nil } func (m *Manager) parseFrontmatter(lines []string) map[string]interface{} { frontmatter := make(map[string]interface{}) for _, line := range lines { line = strings.TrimSpace(line) if line == "---" { break } parts := strings.SplitN(line, ":", 2) if len(parts) == 2 { key := strings.TrimSpace(parts[0]) value := strings.TrimSpace(parts[1]) if strings.HasPrefix(value, "[") && strings.HasSuffix(value, "]") { value = strings.Trim(value, "[]") items := strings.Split(value, ",") var list []interface{} for _, item := range items { list = append(list, strings.TrimSpace(item)) } frontmatter[key] = list } else { frontmatter[key] = value } } } return frontmatter } func (m *Manager) generateSlug(title string) string { if title == "" { return "untitled" } slug := strings.ToLower(title) slug = regexp.MustCompile(`[^a-z0-9\s-]`).ReplaceAllString(slug, "") slug = regexp.MustCompile(`\s+`).ReplaceAllString(slug, "-") slug = strings.Trim(slug, "-") if slug == "" { return "untitled" } return slug } func (m *Manager) generateExcerpt(content string) string { words := strings.Fields(content) if len(words) <= 30 { return content } return strings.Join(words[:30], " ") + "..." } func (m *Manager) GetPost(slug string) (*Post, bool) { post, exists := m.posts[slug] return post, exists } func (m *Manager) GetAllPosts() []*Post { var posts []*Post for _, post := range m.posts { posts = append(posts, post) } sort.Slice(posts, func(i, j int) bool { return posts[i].Date.After(posts[j].Date) }) return posts } func (m *Manager) GetPostsByCategory(category string) []*Post { var posts []*Post for _, post := range m.posts { for _, cat := range post.Categories { if cat == category { posts = append(posts, post) break } } } sort.Slice(posts, func(i, j int) bool { return posts[i].Date.After(posts[j].Date) }) return posts } func (m *Manager) GetPage(path string) (*Page, bool) { page, exists := m.pages[path] return page, exists } func (m *Manager) GetNavigation() []Navigation { return m.navigation } func (m *Manager) GetLastUpdate() time.Time { return m.lastUpdate }