This commit is contained in:
Dev
2025-09-15 04:02:11 +03:00
commit fc86288f06
24 changed files with 2938 additions and 0 deletions

354
internal/content/manager.go Normal file
View 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
}

View 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))
}
})
}
}