gitblog1
This commit is contained in:
354
internal/content/manager.go
Normal file
354
internal/content/manager.go
Normal file
@@ -0,0 +1,354 @@
|
||||
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
|
||||
}
|
110
internal/content/manager_test.go
Normal file
110
internal/content/manager_test.go
Normal file
@@ -0,0 +1,110 @@
|
||||
package content
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestGenerateSlug(t *testing.T) {
|
||||
m := &Manager{}
|
||||
|
||||
tests := []struct {
|
||||
input string
|
||||
expected string
|
||||
}{
|
||||
{"Hello World", "hello-world"},
|
||||
{"Hello, World!", "hello-world"},
|
||||
{"Test@#$%^&*()", "test"},
|
||||
{" Multiple Spaces ", "multiple-spaces"},
|
||||
{"", "untitled"},
|
||||
{"!@#$%", "untitled"},
|
||||
{"123 Test", "123-test"},
|
||||
{"Test-With-Dashes", "test-with-dashes"},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
result := m.generateSlug(test.input)
|
||||
if result != test.expected {
|
||||
t.Errorf("generateSlug(%q) = %q, expected %q", test.input, result, test.expected)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestGenerateExcerpt(t *testing.T) {
|
||||
m := &Manager{}
|
||||
|
||||
tests := []struct {
|
||||
input string
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
"This is a short text",
|
||||
"This is a short text",
|
||||
},
|
||||
{
|
||||
"One two three four five six seven eight nine ten eleven twelve thirteen fourteen fifteen sixteen seventeen eighteen nineteen twenty twenty-one twenty-two twenty-three twenty-four twenty-five twenty-six twenty-seven twenty-eight twenty-nine thirty thirty-one",
|
||||
"One two three four five six seven eight nine ten eleven twelve thirteen fourteen fifteen sixteen seventeen eighteen nineteen twenty twenty-one twenty-two twenty-three twenty-four twenty-five twenty-six twenty-seven twenty-eight twenty-nine thirty...",
|
||||
},
|
||||
{
|
||||
"",
|
||||
"",
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
result := m.generateExcerpt(test.input)
|
||||
if result != test.expected {
|
||||
t.Errorf("generateExcerpt() = %q, expected %q", result, test.expected)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseFrontmatter(t *testing.T) {
|
||||
m := &Manager{}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
lines []string
|
||||
expected map[string]interface{}
|
||||
}{
|
||||
{
|
||||
name: "simple frontmatter",
|
||||
lines: []string{
|
||||
"title: My Post",
|
||||
"date: 2024-01-15",
|
||||
"---",
|
||||
},
|
||||
expected: map[string]interface{}{
|
||||
"title": "My Post",
|
||||
"date": "2024-01-15",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "frontmatter with arrays",
|
||||
lines: []string{
|
||||
"title: My Post",
|
||||
"categories: [Technology, Go]",
|
||||
"tags: [golang, web]",
|
||||
"---",
|
||||
},
|
||||
expected: map[string]interface{}{
|
||||
"title": "My Post",
|
||||
"categories": []interface{}{"Technology", "Go"},
|
||||
"tags": []interface{}{"golang", "web"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "empty frontmatter",
|
||||
lines: []string{},
|
||||
expected: map[string]interface{}{},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
result := m.parseFrontmatter(test.lines)
|
||||
if len(result) != len(test.expected) {
|
||||
t.Errorf("parseFrontmatter() returned %d items, expected %d", len(result), len(test.expected))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user