// parse.go package main import ( "bytes" "fmt" "os" "path/filepath" "regexp" "strings" "time" "git.else-if.org/jess/blog/templates" "github.com/BurntSushi/toml" mathjax "github.com/litao91/goldmark-mathjax" "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" "gopkg.in/yaml.v3" ) // RenderContent parses the file and returns the rendered HTML content as bytes. // It performs no caching. func RenderContent(file ContentFile) ([]byte, error) { // Read Raw File raw, err := ReadRaw(file.OriginalPath) if err != nil { return nil, 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{}, mathjax.MathJax, templates.SidebarTag, // ||| -> templates.TopBanner, ), goldmark.WithRendererOptions( html.WithUnsafe(), ), ) var buf bytes.Buffer ctx := parser.NewContext() if err := md.Convert(raw, &buf, parser.WithContext(ctx)); err != nil { return nil, fmt.Errorf("failed to parse markdown: %w", err) } // Extract Frontmatter // 1. Initialize with External Metadata (if any) var meta templates.PageMetadata if err := loadExternalMetadata(file.OriginalPath, &meta); err != nil { fmt.Printf("Warning: failed to load external metadata for %s: %v\n", file.OriginalPath, err) } // 2. Parse Internal Frontmatter (overrides external) d := frontmatter.Get(ctx) if d != nil { // Decode into a temporary map to capture all fields var rawMap map[string]interface{} if err := d.Decode(&rawMap); err != nil { fmt.Printf("Warning: failed to decode frontmatter for %s: %v\n", file.OriginalPath, err) } else { // Merge into meta if meta.Raw == nil { meta.Raw = make(map[string]interface{}) } for k, v := range rawMap { meta.Raw[k] = v // Also update struct fields manually if needed, or use layout decoding switch strings.ToLower(k) { case "title": if s, ok := v.(string); ok { meta.Title = s } case "stylesheet": if s, ok := v.(string); ok { meta.Stylesheet = s } case "style": if s, ok := v.(string); ok { meta.Style = s } } } } } // 3. Resolve Stylesheet Path if meta.Stylesheet != "" { // If not absolute path (starting with /), resolve relative to content file if !strings.HasPrefix(meta.Stylesheet, "/") { // Get valid web path for the directory containing the file // file.RoutePath is like "/entries/entry1" or "/index" // We want the directory part of the RoutePath dirRoute := filepath.Dir(file.RoutePath) if dirRoute == "." { dirRoute = "" } // Construct new path // e.g. /entries + / + style.css -> /entries/style.css // Clean handles double slashes meta.Stylesheet = filepath.Join(dirRoute, meta.Stylesheet) // Ensure it starts with / for web usage if !strings.HasPrefix(meta.Stylesheet, "/") { meta.Stylesheet = "/" + meta.Stylesheet } } } 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 Home Page, replace content with Latest Post Preview if file.RoutePath == "/" { // Find latest post var latestPost *ContentFile for _, f := range AllContent { if f.IsMarkdown && f.RoutePath != "/" { // Found one latestPost = &f break // AllContent is sorted by time descending } } if latestPost != nil { // Read the latest post postRaw, err := ReadRaw(latestPost.OriginalPath) if err == nil { // Parse the post to HTML to display as preview mdPreview := goldmark.New( goldmark.WithExtensions( extension.GFM, &frontmatter.Extender{}, mathjax.MathJax, templates.SidebarTag, templates.TopBanner, ), goldmark.WithRendererOptions(html.WithUnsafe()), ) var previewBuf bytes.Buffer ctxPreview := parser.NewContext() if err := mdPreview.Convert(postRaw, &previewBuf, parser.WithContext(ctxPreview)); err == nil { // Extract Metadata of the post (unused for now, but kept for future reference) var postMeta templates.PageMetadata dPost := frontmatter.Get(ctxPreview) if dPost != nil { dPost.Decode(&postMeta) } // DO NOT overwrite meta.Title for the Home Page. Keep it as "Index" or whatever index.md has. fullHTML := previewBuf.String() // Extract Preview (First 3 Paragraphs?) // Strategy: count

tags? // Let's capture approx 1500 chars or 3 paragraphs, whichever is shorter/better. // Actually, splitting by

is safer for HTML validation. parts := strings.Split(fullHTML, "

") var previewHTML string limit := 3 if len(parts) < limit { limit = len(parts) } for i := 0; i < limit; i++ { if strings.TrimSpace(parts[i]) != "" { previewHTML += parts[i] + "

" } } // Add Fade/Blur Container // Append to existing htmlContent (which currently holds index.md content) // Add a header for the preview? previewBlock := fmt.Sprintf(`

Latest: %s

%s
`, postMeta.Title, previewHTML, latestPost.RoutePath) htmlContent += previewBlock } } } } // [directory] Tag Expansion // User can put [directory] in any page (e.g. archive.md) to generate the list if strings.Contains(htmlContent, "[directory]") { // Generate Directory HTML (Grouped by Year) var dirHTML strings.Builder dirHTML.WriteString(`
`) // Group by year type YearGroup struct { Year int Posts []templates.PostSnippet } var years []YearGroup for _, f := range AllContent { if f.IsMarkdown && f.RoutePath != "/" { name := filepath.Base(f.OriginalPath) title := strings.TrimSuffix(name, filepath.Ext(name)) // Get Metadata for title? Ideally yes, but costly to parse all? // Let's rely on cached AllContent if we had metadata there, but we don't. // For now, filename title is fast. year := f.ModTime.Year() // Add to current year group or new one if len(years) == 0 || years[len(years)-1].Year != year { years = append(years, YearGroup{Year: year}) } years[len(years)-1].Posts = append(years[len(years)-1].Posts, templates.PostSnippet{ Title: title, URL: f.RoutePath, Date: f.ModTime, }) } } for _, yg := range years { dirHTML.WriteString(fmt.Sprintf(`

%d

`) } dirHTML.WriteString(`
`) htmlContent = strings.ReplaceAll(htmlContent, "[directory]", dirHTML.String()) } /** 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).*?`) if sidebarRegex.MatchString(htmlContent) { // Render the dynamic content // 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, }) } } latestPostsHTML := templates.RenderLatestPosts(posts) dirLink := templates.RenderDirectoryLink() // Wrap in