Files
bamort/backend/pdfrender/template_metadata.go
T
Frank 59fe69d35d refactor: Unify PDF pagination system and rename templates
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
2025-12-21 22:07:46 +01:00

235 lines
6.3 KiB
Go

package pdfrender
import (
"fmt"
"os"
)
// TemplateMetadata contains information about a template's capacity and requirements
type TemplateMetadata struct {
Name string // Template name (e.g., "page_1.html")
PageType string // "stats", "play", "spell", "equip"
Description string
Blocks []BlockMetadata // List blocks and their capacities
}
// BlockMetadata defines a list block within a template and how many items it can hold
type BlockMetadata struct {
Name string // Logical name (e.g., "skills_column1", "weapons_main")
ListType string // "skills", "weapons", "spells", "equipment", "magicItems", "gameResults"
MaxItems int // Maximum number of items this block can display
Filter string // Optional filter criteria (e.g., "learned", "unlearned", "languages")
Column int // Column number for multi-column layouts (0 if single column)
}
// TemplateSet contains all templates and their metadata for a format
type TemplateSet struct {
Name string // e.g., "Default_A4_Quer"
Description string
Templates []TemplateWithMeta
}
// TemplateWithMeta combines a template with its metadata
type TemplateWithMeta struct {
Metadata TemplateMetadata
Path string // File path to the template
}
// LoadTemplateSetFromFiles loads template metadata by parsing actual template files
func LoadTemplateSetFromFiles(templateDir string) (TemplateSet, error) {
templateSet := TemplateSet{
Name: "Default_A4_Quer",
Description: "Standard A4 Querformat Charakterbogen",
Templates: []TemplateWithMeta{},
}
// Define the template files to load
templateFiles := []struct {
filename string
pageType string
description string
}{
{"page_1.html", "stats", "Statistikseite mit Grundwerten"},
{"page_2.html", "play", "Spielbogen mit gelernten Fertigkeiten und Waffen"},
{"page_3.html", "spell", "Zauberseite mit Zauberliste"},
{"page_4.html", "equip", "Ausrüstungsseite"},
}
// Load each template file and parse its metadata
for _, tmplFile := range templateFiles {
filePath := templateDir + "/" + tmplFile.filename
// Read template content
content, err := os.ReadFile(filePath)
if err != nil {
return templateSet, fmt.Errorf("failed to read template %s: %w", tmplFile.filename, err)
}
// Parse metadata from HTML comments
blocks := ParseTemplateMetadata(string(content))
templateSet.Templates = append(templateSet.Templates, TemplateWithMeta{
Metadata: TemplateMetadata{
Name: tmplFile.filename,
PageType: tmplFile.pageType,
Description: tmplFile.description,
Blocks: blocks,
},
Path: filePath,
})
}
return templateSet, nil
}
// DefaultA4QuerTemplateSet returns the template set for A4 Querformat
// Now loads from actual template files instead of hardcoded values
func DefaultA4QuerTemplateSet() TemplateSet {
// Try to load from files
templateSet, err := LoadTemplateSetFromFiles("backend/templates/Default_A4_Quer")
if err != nil {
// Fallback to relative path from test directory
templateSet, err = LoadTemplateSetFromFiles("../templates/Default_A4_Quer")
if err != nil {
// Last fallback: return hardcoded defaults
return getHardcodedTemplateSet()
}
}
return templateSet
}
// getHardcodedTemplateSet returns hardcoded fallback values
func getHardcodedTemplateSet() TemplateSet {
return TemplateSet{
Name: "Default_A4_Quer",
Description: "Standard A4 Querformat Charakterbogen",
Templates: []TemplateWithMeta{
{
Metadata: TemplateMetadata{
Name: "page_1.html",
PageType: "stats",
Description: "Statistikseite mit Grundwerten",
Blocks: []BlockMetadata{
{
Name: "skills_column1",
ListType: "skills",
MaxItems: 29,
Column: 1,
},
{
Name: "skills_column2",
ListType: "skills",
MaxItems: 29,
Column: 2,
},
},
},
Path: "templates/Default_A4_Quer/page_1.html",
},
{
Metadata: TemplateMetadata{
Name: "page_2.html",
PageType: "play",
Description: "Spielbogen mit gelernten Fertigkeiten und Waffen",
Blocks: []BlockMetadata{
{
Name: "skills_learned",
ListType: "skills",
MaxItems: 24,
Filter: "learned",
},
{
Name: "skills_unlearned",
ListType: "skills",
MaxItems: 15,
Filter: "unlearned",
},
{
Name: "skills_languages",
ListType: "skills",
MaxItems: 11,
Filter: "languages",
},
{
Name: "weapons_main",
ListType: "weapons",
MaxItems: 22,
},
},
},
Path: "templates/Default_A4_Quer/page_2.html",
},
{
Metadata: TemplateMetadata{
Name: "page_3.html",
PageType: "spell",
Description: "Zauberseite mit Zauberliste",
Blocks: []BlockMetadata{
{
Name: "spells_left",
ListType: "spells",
MaxItems: 15,
Column: 1,
},
{
Name: "spells_right",
ListType: "spells",
MaxItems: 10,
Column: 2,
},
{
Name: "magic_items",
ListType: "magicItems",
MaxItems: 5,
},
},
},
Path: "templates/Default_A4_Quer/page_3.html",
},
{
Metadata: TemplateMetadata{
Name: "page_4.html",
PageType: "equip",
Description: "Ausrüstungsseite",
Blocks: []BlockMetadata{
{
Name: "equipment_sections",
ListType: "equipment",
MaxItems: 20,
},
{
Name: "game_results",
ListType: "gameResults",
MaxItems: 10,
},
},
},
Path: "templates/Default_A4_Quer/page_4.html",
},
},
}
}
// GetBlockMetadata returns the metadata for a specific block in a template
func (tm *TemplateMetadata) GetBlockMetadata(blockName string) *BlockMetadata {
for i := range tm.Blocks {
if tm.Blocks[i].Name == blockName {
return &tm.Blocks[i]
}
}
return nil
}
// GetMaxItems returns the maximum items for a specific list type in this template
func (tm *TemplateMetadata) GetMaxItems(listType string, filter string) int {
total := 0
for _, block := range tm.Blocks {
if block.ListType == listType {
if filter == "" || block.Filter == filter {
total += block.MaxItems
}
}
}
return total
}