355 lines
7.3 KiB
Go
355 lines
7.3 KiB
Go
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
|
|
}
|