// 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" ) // GetContentPath ensures the content is cached and returns the path to the cached file. func GetContentPath(file ContentFile) (string, error) { cachePath := GetCacheFilename(file.OriginalPath) // 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{}, 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 "", 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 // We need to parse it fully to get the HTML content // Re-using the MD parser config from above would be cleaner but let's instantiate for now 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 to update Page Title? // User said: "the [title] be big text embedded in the black area." // So if we are previewing the latest post, the Home Page Title should probably match the Post Title on the Home Page? // Or should it say "Latest: Title"? // The main header h1 is where {{Title}} goes. // Getting metadata from preview context var postMeta templates.PageMetadata dPost := frontmatter.Get(ctxPreview) if dPost != nil { dPost.Decode(&postMeta) } // Fallback title logic if postMeta.Title == "" { base := filepath.Base(latestPost.OriginalPath) postMeta.Title = strings.TrimSuffix(base, filepath.Ext(base)) } // Update the meta for the HOME PAGE render meta.Title = postMeta.Title // Use the post's title // Extract Preview (First Paragraph?) // "preview of the latest post, then read more" fullHTML := previewBuf.String() // Simple strategy: take everything up to the first

or limit chars? // Or explicit split? // Let's take the first paragraph. parts := strings.Split(fullHTML, "

") if len(parts) > 0 { previewHTML := parts[0] + "

" // Add Read More Link readMore := fmt.Sprintf(`

Read More...

`, latestPost.RoutePath) htmlContent = previewHTML + readMore } else { htmlContent = fullHTML } } } } } // [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