59fe69d35d
BREAKING CHANGE: Template names changed from page1_stats.html to page_1.html ## Phase 1: Unified Pagination Function - Implemented PaginateMultiList() to replace PaginateSkills(), PaginateSpells(), and PaginatePage2PlayLists() - Single metadata-driven function handles all list types (skills, weapons, spells, equipment) - Properly handles filters (learned/unlearned/language) via template metadata - Shares list trackers by ListType+Filter combination to avoid duplication - Added comprehensive tests for all edge cases ## Phase 2: Template Naming Convention - Renamed templates to be data-agnostic: - page1_stats.html -> page_1.html - page1.2_stats.html -> page_1.2.html - page2_play.html -> page_2.html - page2.2_play.html -> page_2.2.html - page3_spell.html -> page_3.html - page3.2_spell.html -> page_3.2.html - page4_equip.html -> page_4.html - Updated GenerateContinuationTemplateName() for new naming (page_1.html -> page_1.2.html) - Updated ExtractBaseTemplateName() to handle new format - Updated all test files and source files with new template names ## Phase 3: Simplified RenderPageWithContinuations - Removed hardcoded switch statements based on template names - Replaced with generic dataMap and unified pagination call - Extracted populatePageDataFromDistribution() to handle data mapping - Template type detection now driven by metadata, not hardcoded names ## Benefits - ✅ Extensibility: Add new templates without code changes - ✅ Maintainability: One pagination algorithm instead of three - ✅ Clarity: Template names reflect page numbers, not content types - ✅ Flexibility: Templates can mix any data types - ✅ All 40+ tests passing ## Technical Details - Added SkillsColumn3 and SkillsColumn4 fields to PageData for continuation pages - Template metadata loaded from HTML comments drives pagination behavior - Backward compatibility maintained for old template references in comments
119 lines
3.3 KiB
Go
119 lines
3.3 KiB
Go
package pdfrender
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"html/template"
|
|
"os"
|
|
"path/filepath"
|
|
)
|
|
|
|
// TemplateLoader manages loading and rendering of HTML templates
|
|
type TemplateLoader struct {
|
|
templateDir string
|
|
templates *template.Template
|
|
metadata map[string][]BlockMetadata
|
|
}
|
|
|
|
// NewTemplateLoader creates a new template loader for the given directory
|
|
func NewTemplateLoader(templateDir string) *TemplateLoader {
|
|
return &TemplateLoader{
|
|
templateDir: templateDir,
|
|
metadata: make(map[string][]BlockMetadata),
|
|
}
|
|
}
|
|
|
|
// LoadTemplates loads all .html templates from the template directory
|
|
func (tl *TemplateLoader) LoadTemplates() error {
|
|
// Check if directory exists
|
|
if _, err := os.Stat(tl.templateDir); os.IsNotExist(err) {
|
|
return fmt.Errorf("template directory does not exist: %s", tl.templateDir)
|
|
}
|
|
|
|
// Find all .html files
|
|
pattern := filepath.Join(tl.templateDir, "*.html")
|
|
files, err := filepath.Glob(pattern)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to find template files: %w", err)
|
|
}
|
|
|
|
if len(files) == 0 {
|
|
return fmt.Errorf("no template files found in %s", tl.templateDir)
|
|
}
|
|
|
|
// Create template with custom functions
|
|
tmpl := template.New("").Funcs(getTemplateFuncs())
|
|
|
|
// Parse all templates
|
|
tmpl, err = tmpl.ParseFiles(files...)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to parse templates: %w", err)
|
|
}
|
|
|
|
tl.templates = tmpl
|
|
|
|
// Extract metadata from each template
|
|
for _, file := range files {
|
|
basename := filepath.Base(file)
|
|
content, err := os.ReadFile(file)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to read template %s: %w", basename, err)
|
|
}
|
|
|
|
metadata := ParseTemplateMetadata(string(content))
|
|
tl.metadata[basename] = metadata
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// RenderTemplate renders a specific template with the given data
|
|
// For continuation templates (e.g., "page_1.2.html"), it automatically
|
|
// falls back to the base template (e.g., "page_1.html") if the continuation
|
|
// template doesn't exist as a physical file
|
|
func (tl *TemplateLoader) RenderTemplate(templateName string, data interface{}) (string, error) {
|
|
if tl.templates == nil {
|
|
return "", fmt.Errorf("templates not loaded, call LoadTemplates first")
|
|
}
|
|
|
|
tmpl := tl.templates.Lookup(templateName)
|
|
if tmpl == nil {
|
|
// Try to extract base template name for continuation pages
|
|
// e.g., "page_1.2.html" -> "page_1.html"
|
|
baseTemplateName := ExtractBaseTemplateName(templateName)
|
|
if baseTemplateName != templateName {
|
|
tmpl = tl.templates.Lookup(baseTemplateName)
|
|
if tmpl == nil {
|
|
return "", fmt.Errorf("template not found: %s (and base template %s not found)", templateName, baseTemplateName)
|
|
}
|
|
} else {
|
|
return "", fmt.Errorf("template not found: %s", templateName)
|
|
}
|
|
}
|
|
|
|
var buf bytes.Buffer
|
|
if err := tmpl.Execute(&buf, data); err != nil {
|
|
return "", fmt.Errorf("failed to render template %s: %w", templateName, err)
|
|
}
|
|
|
|
return buf.String(), nil
|
|
}
|
|
|
|
// GetTemplateMetadata returns the metadata blocks for a given template
|
|
func (tl *TemplateLoader) GetTemplateMetadata(templateName string) []BlockMetadata {
|
|
return tl.metadata[templateName]
|
|
}
|
|
|
|
// getTemplateFuncs returns custom template functions
|
|
func getTemplateFuncs() template.FuncMap {
|
|
return template.FuncMap{
|
|
"iterate": func(n int) []int {
|
|
result := make([]int, n)
|
|
for i := 0; i < n; i++ {
|
|
result[i] = i
|
|
}
|
|
return result
|
|
},
|
|
}
|
|
}
|