blog/parse.go

176 lines
4.8 KiB
Go

// parse.go
package main
import (
"bytes"
"fmt"
"os"
"path/filepath"
"regexp"
"strings"
"git.else-if.org/jess/blog/templates"
"github.com/yuin/goldmark"
"github.com/yuin/goldmark/extension"
"github.com/yuin/goldmark/parser"
"github.com/yuin/goldmark/renderer/html"
"go.abhg.dev/goldmark/frontmatter"
)
// GetContentPath ensures the content is cached and returns the path to the cached file.
func GetContentPath(file ContentFile) (string, error) {
cachePath := GetCacheFilename(file.OriginalPath)
// 1. Check Cache Freshness (if not in TestMode)
if !TestMode {
cacheInfo, errCache := os.Stat(cachePath)
origInfo, errOrig := os.Stat(file.OriginalPath)
if errCache == nil && errOrig == nil && cacheInfo.ModTime().After(origInfo.ModTime()) {
return cachePath, nil
}
}
// 2. Regenerate Content
cacheMutex.Lock()
defer cacheMutex.Unlock()
if !TestMode {
cacheInfo, errCache := os.Stat(cachePath)
origInfo, errOrig := os.Stat(file.OriginalPath)
if errCache == nil && errOrig == nil && cacheInfo.ModTime().After(origInfo.ModTime()) {
return cachePath, nil
}
}
// Read Raw File
raw, err := ReadRaw(file.OriginalPath)
if err != nil {
return "", fmt.Errorf("failed to read file: %w", err)
}
var dataToWrite []byte
if !file.IsMarkdown {
dataToWrite = raw
} else {
// Configure Goldmark with the custom tag extension
md := goldmark.New(
goldmark.WithExtensions(
extension.GFM,
&frontmatter.Extender{},
templates.SidebarTag, // ||| -> <sidebar>
templates.TopBanner,
templates.Mathtext,
),
goldmark.WithRendererOptions(
html.WithUnsafe(),
),
)
var buf bytes.Buffer
ctx := parser.NewContext()
if err := md.Convert(raw, &buf, parser.WithContext(ctx)); err != nil {
return "", fmt.Errorf("failed to parse markdown: %w", err)
}
// Extract Frontmatter
var meta templates.PageMetadata
d := frontmatter.Get(ctx)
if d != nil {
if err := d.Decode(&meta); err != nil {
fmt.Printf("Warning: failed to decode frontmatter for %s: %v\n", file.OriginalPath, err)
}
}
if meta.Title == "" {
base := filepath.Base(file.OriginalPath)
meta.Title = strings.TrimSuffix(base, filepath.Ext(base))
}
htmlContent := buf.String()
// 3. Post-Process for Index (Dynamic Content Injection)
if file.RoutePath == "/" {
// Generate posts data
var posts []templates.PostSnippet
for _, f := range AllContent {
if f.IsMarkdown && f.RoutePath != "/" {
name := filepath.Base(f.OriginalPath)
title := strings.TrimSuffix(name, filepath.Ext(name))
posts = append(posts, templates.PostSnippet{
Title: title,
URL: f.RoutePath,
Date: f.ModTime,
})
}
}
/**
Add More Custom tags here. Add a customTag in tags.go as well, and the implementation in a template file.
*/
// ||| SideBar
sidebarRegex := regexp.MustCompile(`(?s)<sidebar>.*?</sidebar>`)
if sidebarRegex.MatchString(htmlContent) {
// Render the dynamic content
latestPostsHTML := templates.RenderLatestPosts(posts)
dirLink := templates.RenderDirectoryLink()
// Wrap in <div class="sidebar"> to match CSS
replacement := fmt.Sprintf(`<div class="sidebar">%s%s</div>`, latestPostsHTML, dirLink)
// Replace the placeholder tag with the actual content
htmlContent = sidebarRegex.ReplaceAllString(htmlContent, replacement)
}
// _-_- TopBanner
topbannerRegex := regexp.MustCompile(`(?s)<topbanner>.*?</topbanner>`)
if topbannerRegex.MatchString(htmlContent) {
htmlContent = topbannerRegex.ReplaceAllStringFunc(htmlContent, func(match string) string {
// 'match' is "<topbanner>...content...</topbanner>"
// Remove the known tags safely
innerContent := strings.TrimPrefix(match, "<topbanner>")
innerContent = strings.TrimSuffix(innerContent, "</topbanner>")
// Pass to template
bannerHTML := templates.RenderTopBanner(innerContent)
// Return the formatted HTML
return fmt.Sprintf(`<div class="topbanner">%s</div>`, bannerHTML)
})
}
}
// Mathtext mathtext
mathtextRegex := regexp.MustCompile("(?s)<mathtext>.*?</mathtext>")
if mathtextRegex.MatchString(htmlContent) {
htmlContent = mathtextRegex.ReplaceAllStringFunc(htmlContent, func(match string) string {
innerContent := strings.TrimPrefix(match, "<mathtext>")
innerContent = strings.TrimSuffix(innerContent, "</mathtext>")
return fmt.Sprintf("<li class=\"mathtext\">%s</li>", innerContent)
})
}
// Build Full Page
dataToWrite = templates.BuildFullPage([]byte(htmlContent), meta)
}
// 4. Write to Cache
tmpPath := cachePath + ".tmp"
if err := os.WriteFile(tmpPath, dataToWrite, 0644); err != nil {
return "", fmt.Errorf("failed to write cache: %w", err)
}
if err := os.Rename(tmpPath, cachePath); err != nil {
return "", fmt.Errorf("failed to commit cache: %w", err)
}
return cachePath, nil
}